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

witseie-elen4010 / 2026-group-lab-002 / 25968699004

16 May 2026 05:44PM UTC coverage: 88.376% (+1.3%) from 87.104%
25968699004

push

github

Aditya-Raghunandan
fix(ci): remove recursive dependencies lifecycle script causing npm ci infinite loop

494 of 600 branches covered (82.33%)

Branch coverage included in aggregate %.

1095 of 1198 relevant lines covered (91.4%)

11.61 hits per line

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

91.1
/src/controllers/auth-controller.js
1
const crypto = require('crypto')
11✔
2
const db = require('../../database/db')
11✔
3
const { logActivity } = require('../services/logging-service')
11✔
4
const ActionTypes = require('../services/action-types')
11✔
5
const bcryptjs = require('bcryptjs')
11✔
6
const { sendLoginWarningEmail } = require('../services/email-service')
11✔
7

8
const showLogin = (req, res) => {
11✔
9
  if (req.session && req.session.userId) {
5✔
10
    const role = req.session.userRole
3✔
11
    if (role === 'student') return res.redirect('/student/dashboard')
3✔
12
    if (role === 'admin') return res.redirect('/admin/dashboard')
2✔
13
    return res.redirect('/lecturer/dashboard')
1✔
14
  }
15
  return res.render('login', { error: null, success: req.query.success || null })
2✔
16
}
17

18
const _recordFailedAttempt = async (userId, table, email) => {
11✔
19
  const idCol = table === 'staff' ? 'staff_number' : 'student_number'
18✔
20

21
  db.prepare(`UPDATE ${table} SET failed_attempts = failed_attempts + 1 WHERE ${idCol} = ?`).run(userId)
18✔
22
  const row = db.prepare(`SELECT failed_attempts FROM ${table} WHERE ${idCol} = ?`).get(userId)
18✔
23
  const attempts = row ? row.failed_attempts : 1
18!
24

25
  const pinTriggered = attempts === 4 ? 1 : 0
18✔
26
  db.prepare('INSERT INTO failed_login_log (identifier, pin_triggered) VALUES (?, ?)').run(String(userId), pinTriggered)
18✔
27

28
  let pinSent = false
18✔
29
  if (attempts === 4) {
18✔
30
    const pin = String(Math.floor(100000 + Math.random() * 900000))
4✔
31
    const pinHash = crypto.createHash('sha256').update(pin).digest('hex')
4✔
32
    db.prepare(`UPDATE ${table} SET login_pin = ? WHERE ${idCol} = ?`).run(pinHash, userId)
4✔
33
    try {
4✔
34
      await sendLoginWarningEmail(email, pin)
4✔
35
      pinSent = true
1✔
36
    } catch (err) {
37
      console.error('Login warning email failed to send:', err)
3✔
38
    }
39
  }
40

41
  await logActivity(String(userId), ActionTypes.AUTH_FAILED_LOGIN, [])
18✔
42
  return { attempts, pinSent }
18✔
43
}
44

45
const login = async (req, res) => {
11✔
46
  try {
61✔
47
    const { staffStudentNumber, password } = req.body
61✔
48

49
    if (!staffStudentNumber || !password) {
61!
50
      return res.render('login', { error: 'Please enter both your user number and password.', success: null })
×
51
    }
52

53
    // 1. Staff Check
54
    const staff = db.prepare('SELECT * FROM staff WHERE staff_number = ?').get(staffStudentNumber)
61✔
55
    if (staff) {
61✔
56
      if (staff.login_pin) {
22✔
57
        req.session.pendingUserId = staff.staff_number
1✔
58
        req.session.pendingUserRole = 'lecturer'
1✔
59
        req.session.pendingUserName = staff.name
1✔
60
        return res.redirect('/login/pin')
1✔
61
      }
62

63
      const isMatch = await bcryptjs.compare(password, staff.password)
21✔
64
      
65
      if (isMatch) {
21✔
66
        if (!staff.email_verified) {
18✔
67
          return res.render('login', {
1✔
68
            error: 'Your email address has not been verified. Please check your inbox.',
69
            success: null,
70
          })
71
        }
72
        
73
        db.prepare('UPDATE staff SET failed_attempts = 0 WHERE staff_number = ?').run(staff.staff_number)
17✔
74
        req.session.userId = staff.staff_number
17✔
75
        req.session.userName = staff.name
17✔
76
        req.session.userRole = 'lecturer'
17✔
77
        req.session.showWelcome = true
17✔
78
        await logActivity(staff.staff_number, ActionTypes.USER_LOGIN, [])
17✔
79
        return res.redirect('/lecturer/dashboard?welcome=1')
17✔
80
      } 
81
      
82
      const { attempts, pinSent } = await _recordFailedAttempt(staff.staff_number, 'staff', staff.email)
3✔
83
      if (attempts >= 4) {
3✔
84
        const msg = pinSent
1!
85
          ? 'Too many failed attempts. A security PIN has been sent to your email — you will need it to log in.'
86
          : 'Too many failed attempts. A security PIN was already sent to your email. Check your inbox.'
87
        return res.render('login', { error: msg, success: null })
1✔
88
      }
89
      
90
      const remaining = 4 - attempts
2✔
91
      return res.render('login', {
2✔
92
        error: `Invalid password. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining before account lockout.`,
2!
93
        success: null
94
      })
95
    }
96

97
    // 2. Student Check
98
    const student = db.prepare('SELECT * FROM students WHERE student_number = ?').get(staffStudentNumber)
39✔
99
    if (student) {
39✔
100
      if (student.login_pin) {
37✔
101
        req.session.pendingUserId = student.student_number
7✔
102
        req.session.pendingUserRole = 'student'
7✔
103
        req.session.pendingUserName = student.name
7✔
104
        return res.redirect('/login/pin')
7✔
105
      }
106

107
      const isMatch = await bcryptjs.compare(password, student.password)
30✔
108
      
109
      if (isMatch) {
30✔
110
        if (!student.email_verified) {
15✔
111
          return res.render('login', {
2✔
112
            error: 'Your email address has not been verified. Please check your inbox.',
113
            success: null,
114
          })
115
        }
116
        
117
        db.prepare('UPDATE students SET failed_attempts = 0 WHERE student_number = ?').run(student.student_number)
13✔
118
        req.session.userId = student.student_number
13✔
119
        req.session.userName = student.name
13✔
120
        req.session.userRole = 'student'
13✔
121
        req.session.showWelcome = true
13✔
122
        await logActivity(student.student_number, ActionTypes.USER_LOGIN, [])
13✔
123
        return res.redirect('/student/dashboard?welcome=1')
13✔
124
      } 
125
      
126
      const { attempts, pinSent } = await _recordFailedAttempt(student.student_number, 'students', student.email)
15✔
127
      if (attempts >= 4) {
15✔
128
        const msg = pinSent
3!
129
          ? 'Too many failed attempts. A security PIN has been sent to your email — you will need it to log in.'
130
          : 'Too many failed attempts. A security PIN was already sent to your email. Check your inbox.'
131
        return res.render('login', { error: msg, success: null })
3✔
132
      }
133
      
134
      const remaining = 4 - attempts
12✔
135
      return res.render('login', {
12✔
136
        error: `Invalid password. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining before account lockout.`,
12✔
137
        success: null
138
      })
139
    }
140

141
    // 3. Admin Check
142
    let admin = null
2✔
143
    try { 
2✔
144
      admin = db.prepare('SELECT * FROM admins WHERE admin_id = ?').get(staffStudentNumber) 
2✔
145
    } catch (_) {} 
146
    
147
    if (admin) {
2✔
148
      const isMatch = await bcryptjs.compare(password, admin.password)
1✔
149
      if (isMatch) {
1!
150
        req.session.userId = admin.admin_id
1✔
151
        req.session.userName = admin.name
1✔
152
        req.session.userRole = 'admin'
1✔
153
        await logActivity(admin.admin_id, ActionTypes.USER_LOGIN, [])
1✔
154
        return res.redirect('/admin/dashboard')
1✔
155
      } else { 
156
        await logActivity(admin.admin_id, ActionTypes.AUTH_FAILED_LOGIN, [])
×
157
        return res.render('login', { error: 'Invalid password.', success: null })
×
158
      }
159
    } 
160

161
    // 4. Fallback: No user matched
162
    await logActivity(staffStudentNumber || 'UNKNOWN', ActionTypes.AUTH_FAILED_LOGIN, [])
1!
163
    return res.render('login', { error: 'Invalid user number.', success: null })
1✔
164

165
  } catch (error) {
166
    console.error('Login error:', error)
×
167
    return res.render('login', { error: 'Your email address has not been verified. Please check your inbox.', success: null })
×
168
  }
169
}
170

171
const showLoginPin = (req, res) => {
11✔
172
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
173
  return res.render('login-pin', { error: null, message: null })
1✔
174
}
175

176
const resendLoginPin = async (req, res) => {
11✔
177
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
178

179
  const userId = req.session.pendingUserId
1✔
180
  const role = req.session.pendingUserRole
1✔
181
  const table = role === 'lecturer' ? 'staff' : 'students'
1!
182
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
1!
183

184
  const user = db.prepare(`SELECT email FROM ${table} WHERE ${idCol} = ?`).get(userId)
1✔
185
  if (!user) return res.redirect('/login')
1!
186

187
  const pin = String(Math.floor(100000 + Math.random() * 900000))
1✔
188
  const pinHash = crypto.createHash('sha256').update(pin).digest('hex')
1✔
189
  db.prepare(`UPDATE ${table} SET login_pin = ? WHERE ${idCol} = ?`).run(pinHash, userId)
1✔
190

191
  try {
1✔
192
    await sendLoginWarningEmail(user.email, pin)
1✔
193
    return res.render('login-pin', { error: null, message: 'A new PIN has been sent to your email.' })
1✔
194
  } catch (err) {
195
    console.error('Resend PIN email failed:', err)
×
196
    return res.render('login-pin', { error: 'Failed to resend PIN. Please try again.', message: null })
×
197
  }
198
}
199

200
const verifyLoginPin = async (req, res) => {
11✔
201
  if (!req.session.pendingUserId) return res.redirect('/login')
4✔
202

203
  const { pin } = req.body
3✔
204
  const userId = req.session.pendingUserId
3✔
205
  const role = req.session.pendingUserRole
3✔
206
  const name = req.session.pendingUserName
3✔
207

208
  const table = role === 'lecturer' ? 'staff' : 'students'
3✔
209
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
3✔
210

211
  const user = db.prepare(`SELECT login_pin FROM ${table} WHERE ${idCol} = ?`).get(userId)
3✔
212

213
  if (!user || !user.login_pin) {
3!
214
    delete req.session.pendingUserId
×
215
    return res.redirect('/login')
×
216
  }
217

218
  const pinHash = crypto.createHash('sha256').update(pin).digest('hex')
3✔
219
  if (pinHash !== user.login_pin) {
3✔
220
    return res.render('login-pin', { error: 'Incorrect PIN. Check your security alert email and try again.', message: null })
1✔
221
  }
222

223
  db.prepare(`UPDATE ${table} SET login_pin = NULL, failed_attempts = 0 WHERE ${idCol} = ?`).run(userId)
2✔
224

225
  delete req.session.pendingUserId
2✔
226
  delete req.session.pendingUserRole
2✔
227
  delete req.session.pendingUserName
2✔
228
  req.session.userId = userId
2✔
229
  req.session.userName = name
2✔
230
  req.session.userRole = role
2✔
231
  req.session.showWelcome = true
2✔
232

233
  await logActivity(String(userId), ActionTypes.USER_LOGIN, [])
2✔
234

235
  const dashUrl = role === 'lecturer' ? '/lecturer/dashboard?welcome=1' : '/student/dashboard?welcome=1'
2✔
236
  return res.redirect(dashUrl)
2✔
237
}
238

239
const logout = async (req, res) => {
11✔
240
  if (req.session && req.session.userId) {
2!
241
    await logActivity(req.session.userId, ActionTypes.USER_LOGOUT, [])
2✔
242
  }
243

244
  req.session.destroy(() => {
2✔
245
    res.redirect('/')
2✔
246
  })
247
}
248

249
module.exports = { showLogin, login, logout, showLoginPin, resendLoginPin, verifyLoginPin }
11✔
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