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

Seniru / defendxstore / 14561851007

20 Apr 2025 05:38PM UTC coverage: 54.052% (-0.3%) from 54.383%
14561851007

push

github

web-flow
feat: reporting system (#65)

* feat: reporting schemas and report crud

* feat: user logs ui

* update: add logs for all user actions

* feat: report filtering

- filter user reports with
  - username
  - fromDate, toDate
  - type

* refactor: use Schema.Types.ObjectId

mongoose.Schema.Types.ObjectId instead of mongoose.Types.ObjectId to align with common Mongoose schema conventions.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

100 of 254 branches covered (39.37%)

Branch coverage included in aggregate %.

32 of 48 new or added lines in 6 files covered. (66.67%)

2 existing lines in 2 files now uncovered.

507 of 869 relevant lines covered (58.34%)

12.12 hits per line

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

85.94
/backend/src/controllers/users.js
1
require("dotenv").config()
3✔
2
const mongoose = require("mongoose")
3✔
3
const bcrypt = require("bcrypt")
3✔
4
const jwt = require("jsonwebtoken")
3✔
5
const { StatusCodes } = require("http-status-codes")
3✔
6

7
const createResponse = require("../utils/createResponse")
3✔
8
const createToken = require("../utils/createToken")
3✔
9
const User = require("../models/User")
3✔
10
const logger = require("../utils/logger")
3✔
11
const { sendMail } = require("../services/email")
3✔
12
const UserReport = require("../models/reports/UserReport")
3✔
13

14
const permissions = {
3✔
15
    USER: 1 << 0,
16
    DELIVERY_AGENT: 1 << 1,
17
    SUPPORT_AGENT: 1 << 2,
18
    ADMIN: 1 << 3,
19
}
20

21
const getAllUsers = async (req, res, next) => {
3✔
22
    try {
33✔
23
        const search = req.query.search || ""
33✔
24
        const type = req.query.type
33✔
25

26
        if (type && !["USER", "SUPPORT_AGENT", "DELIVERY_AGENT", "ADMIN"].includes(type))
33✔
27
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid type")
3✔
28

29
        let users = await User.find(
30✔
30
            { username: { $regex: search, $options: "i" } },
31
            {
32
                username: 1,
33
                email: 1,
34
                deliveryAddress: 1,
35
                contactNumber: 1,
36
                role: 1,
37
                profileImage: {
38
                    $cond: {
39
                        if: { $ifNull: ["$profileImage", false] },
40
                        then: {
41
                            $concat: [
42
                                `${req.protocol}://${req.get("host")}/api/users/`,
43
                                "$username",
44
                                "/profileImage",
45
                            ],
46
                        },
47
                        else: null,
48
                    },
49
                },
50
                verified: 1,
51
            },
52
        ).exec()
53

54
        users = users.map((user) => user.applyDerivations())
294✔
55
        // apply filters
56
        if (type) users = users.filter((user) => user.role.includes(type))
225✔
57

58
        return createResponse(res, StatusCodes.OK, { users })
30✔
59
    } catch (error) {
60
        next(error)
×
61
    }
62
}
63

64
const createUser = async (req, res, next) => {
3✔
65
    try {
27✔
66
        const {
67
            username,
68
            email,
69
            password,
70
            deliveryAddress,
71
            contactNumber,
72
            profileImage,
73
            referredBy,
74
        } = req.body
27✔
75
        if (!password)
27✔
76
            return createResponse(res, StatusCodes.BAD_REQUEST, [
3✔
77
                {
78
                    field: "password",
79
                    message: "You must provide a password",
80
                },
81
            ])
82
        // check if profileImage is in the correct format
83
        if (profileImage && !profileImage.match(/^data:(.+);base64,(.*)$/))
24✔
84
            return createResponse(res, StatusCodes.BAD_REQUEST, [
3✔
85
                {
86
                    field: "profileImage",
87
                    message: "Invalid profile image format",
88
                },
89
            ])
90
        // check if referredBy is in valid format if it exist
91
        if (referredBy && !mongoose.Types.ObjectId.isValid(referredBy))
21!
92
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid referredBy ID")
×
93

94
        const salt = await bcrypt.genSalt(10)
21✔
95
        const hashedPassword = await bcrypt.hash(password, salt)
21✔
96
        const user = new User({
21✔
97
            username,
98
            email,
99
            password: hashedPassword,
100
            deliveryAddress,
101
            contactNumber,
102
            profileImage,
103
        })
104
        await user.save()
21✔
105
        const token = createToken(user)
6✔
106

107
        // create verification token
108
        const verificationToken = jwt.sign(
6✔
109
            { email, action: "EMAIL_VERIFICATION" },
110
            process.env.JWT_SECRET,
111
            {
112
                algorithm: "HS256",
113
                expiresIn: "1h",
114
            },
115
        )
116

117
        await UserReport.create({
6✔
118
            user: user._id,
119
            action: UserReport.actions.createAccount,
120
            data: {},
121
        })
122

123
        // send verification email
124
        sendMail(email, "Defendxstore Email verification", "verify-email", {
6✔
125
            username,
126
            url: `${process.env.FRONTEND_URL}/verify?token=${verificationToken}`,
127
        })
128
        user.pushNotification("Welcome to DefendX! Check your inbox to verify your email")
6✔
129

130
        // handle referrals
131
        if (referredBy) {
6!
132
            const referredUser = await User.findById(referredBy).exec()
×
133
            if (!referredUser) return createResponse(res, StatusCodes.CREATED, { token })
×
134
            if (!referredUser.verified) return createResponse(res, StatusCodes.CREATED, { token })
×
135
            referredUser.referrals.push(user._id)
×
136
            await referredUser.save()
×
137
            await User.findByIdAndUpdate(
×
138
                user._id,
139
                { referredBy: referredUser._id },
140
                { new: true, runValidators: true },
141
            )
NEW
142
            await UserReport.create({
×
143
                user: user._id,
144
                action: UserReport.actions.referral,
145
                data: { referredUser },
146
            })
UNCOV
147
            referredUser.pushNotification(
×
148
                `You were referred by ${user.username}! Ask them to verify their account to enjoy special discounts.`,
149
            )
150
        }
151

152
        return createResponse(res, StatusCodes.CREATED, { token })
6✔
153
    } catch (error) {
154
        if (error instanceof mongoose.Error.ValidationError) {
15✔
155
            return createResponse(
9✔
156
                res,
157
                StatusCodes.BAD_REQUEST,
158
                Object.keys(error.errors).map((key) => ({
9✔
159
                    field: key,
160
                    message: error.errors[key].message,
161
                })),
162
            )
163
        } else if (error.message == "User already exist with this email") {
6✔
164
            return createResponse(res, StatusCodes.CONFLICT, [
3✔
165
                {
166
                    field: "email",
167
                    message: error.message,
168
                },
169
            ])
170
        } else if (error.message == "Username taken") {
3!
171
            return createResponse(res, StatusCodes.CONFLICT, [
3✔
172
                {
173
                    field: "username",
174
                    message: error.message,
175
                },
176
            ])
177
        }
178
        next(error)
×
179
    }
180
}
181

182
const deleteUser = async (req, res, next) => {
3✔
183
    try {
12✔
184
        const { username } = req.params
12✔
185
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
12✔
186
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot delete this user")
3✔
187

188
        const user = await User.findOneAndDelete({ username }).exec()
9✔
189
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
190
        await UserReport.create({
6✔
191
            user: user._id,
192
            action: UserReport.actions.deleteAccount,
193
            data: {},
194
        })
195
        return createResponse(res, StatusCodes.OK, "User deleted")
6✔
196
    } catch (error) {
197
        next(error)
×
198
    }
199
}
200

201
const getUser = async (req, res, next) => {
3✔
202
    try {
15✔
203
        const { username } = req.params
15✔
204
        let user = await User.findOne({ username }, "-password").exec()
15✔
205
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
15✔
206
        user = user.applyDerivations()
12✔
207
        if (
12✔
208
            !req.user.roles.includes("ADMIN") &&
27✔
209
            req.user.username !== username &&
210
            !user.role.includes("DELIVERY_AGENT")
211
        )
212
            return createResponse(
3✔
213
                res,
214
                StatusCodes.FORBIDDEN,
215
                "You are not authorized to view this user",
216
            )
217

218
        return createResponse(res, StatusCodes.OK, { user })
9✔
219
    } catch (error) {
220
        next(error)
×
221
    }
222
}
223

224
const changePassword = async (req, res, next) => {
3✔
225
    try {
12✔
226
        const { username } = req.params
12✔
227
        const { password } = req.body
12✔
228
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
12✔
229
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
230

231
        if (!password || password.toString() === "")
9!
232
            return createResponse(res, StatusCodes.BAD_REQUEST, "You must provide the password")
×
233

234
        const salt = await bcrypt.genSalt(10)
9✔
235
        const hashedPassword = await bcrypt.hash(password, salt)
9✔
236

237
        const user = await User.findOneAndUpdate({ username }, { password: hashedPassword }).exec()
9✔
238
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
239

240
        await UserReport.create({
6✔
241
            user: user._id,
242
            action: UserReport.actions.changePassword,
243
            data: {},
244
        })
245
        return createResponse(res, StatusCodes.OK, "Password changed")
6✔
246
    } catch (error) {
247
        next(error)
×
248
    }
249
}
250

251
const getUserProfileImage = async (req, res, next) => {
3✔
252
    try {
9✔
253
        const { username } = req.params
9✔
254
        const user = await User.findOne({ username })
9✔
255
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
256
        // return image
257
        const { profileImage } = user
6✔
258
        if (!profileImage)
6✔
259
            return createResponse(res, StatusCodes.NOT_FOUND, "No profile image found")
3✔
260

261
        const match = profileImage.match(/^data:(.+);base64,(.*)$/)
3✔
262

263
        const fileType = match[1]
3✔
264
        const imageData = match[2]
3✔
265

266
        res.status(StatusCodes.OK)
3✔
267
            .set({ "Content-Type": fileType })
268
            .send(Buffer.from(imageData, "base64"))
269
    } catch (error) {
270
        next(error)
×
271
    }
272
}
273

274
const changeProfileImage = async (req, res, next) => {
3✔
275
    try {
21✔
276
        const { username } = req.params
21✔
277
        const { image } = req.body
21✔
278

279
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
21✔
280
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
281

282
        if (!image || !image.match(/^data:(.+);base64,(.*)$/))
18✔
283
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid profile image format")
6✔
284

285
        const user = await User.findOneAndUpdate(
12✔
286
            { username },
287
            { profileImage: image },
288
            { runValidators: true },
289
        ).exec()
290
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
291
        await UserReport.create({
6✔
292
            user: user._id,
293
            action: UserReport.actions.changeProfileImage,
294
            data: {},
295
        })
296
        return createResponse(res, StatusCodes.OK, "Image updated successfully")
6✔
297
    } catch (error) {
298
        if (error instanceof mongoose.Error.ValidationError) {
3!
299
            return createResponse(
3✔
300
                res,
301
                StatusCodes.BAD_REQUEST,
302
                Object.keys(error.errors).map((key) => ({
3✔
303
                    field: key,
304
                    message: error.errors[key].message,
305
                })),
306
            )
307
        }
308
        next(error)
×
309
    }
310
}
311

312
const addRole = async (req, res, next) => {
3✔
313
    try {
21✔
314
        const reqUser = await User.findOne({ username: req.user.username }).exec()
21✔
315
        const { username } = req.params
21✔
316
        const { role } = req.body
21✔
317
        if (!role || !["USER", "DELIVERY_AGENT", "SUPPORT_AGENT", "ADMIN"].includes(role))
21✔
318
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid role")
3✔
319

320
        const user = await User.findOne({ username })
18✔
321
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
18✔
322

323
        user.role |= permissions[role]
15✔
324
        await user.save()
15✔
325
        await UserReport.create({
15✔
326
            user: reqUser._id,
327
            action: UserReport.actions.addRole,
328
            data: { role, username: user.username },
329
        })
330
        return createResponse(res, StatusCodes.OK, "Role added")
15✔
331
    } catch (error) {
332
        next(error)
×
333
    }
334
}
335

336
const removeRole = async (req, res, next) => {
3✔
337
    try {
18✔
338
        const reqUser = await User.findOne({ username: req.user.username }).exec()
18✔
339
        const { username, role } = req.params
18✔
340
        if (!["USER", "DELIVERY_AGENT", "SUPPORT_AGENT", "ADMIN"].includes(role))
18✔
341
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid role")
3✔
342

343
        const user = await User.findOne({ username }).exec()
15✔
344
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
15✔
345

346
        user.role &= (2 ** Object.keys(permissions).length - 1) & ~permissions[role]
12✔
347
        await user.save()
12✔
348
        await UserReport.create({
12✔
349
            user: reqUser._id,
350
            action: UserReport.actions.removeRole,
351
            data: { role, username: user.username },
352
        })
353
        return createResponse(res, StatusCodes.OK, "Role removed")
12✔
354
    } catch (error) {
355
        next(error)
×
356
    }
357
}
358

359
const editUser = async (req, res, next) => {
3✔
360
    try {
9✔
361
        const { username } = req.params
9✔
362
        const { deliveryAddress, contactNumber } = req.body
9✔
363

364
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
9✔
365
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
366

367
        let update = { deliveryAddress }
6✔
368
        if (contactNumber) update.contactNumber = [contactNumber]
6!
369

370
        const user = await User.findOneAndUpdate({ username }, update)
6✔
371

372
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
6✔
373
        await UserReport.create({
3✔
374
            user: user._id,
375
            action: UserReport.actions.editProfile,
376
            data: {},
377
        })
378
        return createResponse(res, StatusCodes.OK, "Editted")
3✔
379
    } catch (error) {
380
        if (error instanceof mongoose.Error.ValidationError) {
×
381
            return createResponse(
×
382
                res,
383
                StatusCodes.BAD_REQUEST,
384
                Object.keys(error.errors).map((key) => ({
×
385
                    field: key,
386
                    message: error.errors[key].message,
387
                })),
388
            )
389
        }
390
        next(error)
×
391
    }
392
}
393

394
module.exports = {
3✔
395
    getAllUsers,
396
    createUser,
397
    deleteUser,
398
    getUser,
399
    changePassword,
400
    getUserProfileImage,
401
    changeProfileImage,
402
    addRole,
403
    removeRole,
404
    editUser,
405
}
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