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

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

16 May 2026 05:57PM UTC coverage: 87.998% (-0.4%) from 88.376%
25968964238

push

github

web-flow
Merge pull request #123 from witseie-elen4010/passwordChange

Password change and bug fix

525 of 640 branches covered (82.03%)

Branch coverage included in aggregate %.

62 of 75 new or added lines in 5 files covered. (82.67%)

1 existing line in 1 file now uncovered.

1154 of 1268 relevant lines covered (91.01%)

11.21 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.redirect(`/verify-email?email=${encodeURIComponent(staff.email)}&fromLogin=1`)
1✔
68
        }
69

70
        db.prepare('UPDATE staff SET failed_attempts = 0 WHERE staff_number = ?').run(staff.staff_number)
17✔
71
        req.session.userId = staff.staff_number
17✔
72
        req.session.userName = staff.name
17✔
73
        req.session.userRole = 'lecturer'
17✔
74
        req.session.showWelcome = true
17✔
75
        await logActivity(staff.staff_number, ActionTypes.USER_LOGIN, [])
17✔
76
        return res.redirect('/lecturer/dashboard?welcome=1')
17✔
77
      }
78

79
      const { attempts, pinSent } = await _recordFailedAttempt(staff.staff_number, 'staff', staff.email)
3✔
80
      if (attempts >= 4) {
3✔
81
        const msg = pinSent
1!
82
          ? 'Too many failed attempts. A security PIN has been sent to your email — you will need it to log in.'
83
          : 'Too many failed attempts. A security PIN was already sent to your email. Check your inbox.'
84
        return res.render('login', { error: msg, success: null })
1✔
85
      }
86
      
87
      const remaining = 4 - attempts
2✔
88
      return res.render('login', {
2✔
89
        error: `Invalid password. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining before account lockout.`,
2!
90
        success: null
91
      })
92
    }
93

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

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

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

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

159
  } catch (error) {
160
    console.error('Login error:', error)
×
NEW
161
    return res.render('login', { error: 'An unexpected error occurred. Please try again.', success: null })
×
162
  }
163
}
164

165
const showLoginPin = (req, res) => {
11✔
166
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
167
  return res.render('login-pin', { error: null, message: null })
1✔
168
}
169

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

173
  const userId = req.session.pendingUserId
1✔
174
  const role = req.session.pendingUserRole
1✔
175
  const table = role === 'lecturer' ? 'staff' : 'students'
1!
176
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
1!
177

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

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

185
  try {
1✔
186
    await sendLoginWarningEmail(user.email, pin)
1✔
187
    return res.render('login-pin', { error: null, message: 'A new PIN has been sent to your email.' })
1✔
188
  } catch (err) {
189
    console.error('Resend PIN email failed:', err)
×
190
    return res.render('login-pin', { error: 'Failed to resend PIN. Please try again.', message: null })
×
191
  }
192
}
193

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

197
  const { pin } = req.body
3✔
198
  const userId = req.session.pendingUserId
3✔
199
  const role = req.session.pendingUserRole
3✔
200
  const name = req.session.pendingUserName
3✔
201

202
  const table = role === 'lecturer' ? 'staff' : 'students'
3✔
203
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
3✔
204

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

207
  if (!user || !user.login_pin) {
3!
208
    delete req.session.pendingUserId
×
209
    return res.redirect('/login')
×
210
  }
211

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

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

219
  delete req.session.pendingUserId
2✔
220
  delete req.session.pendingUserRole
2✔
221
  delete req.session.pendingUserName
2✔
222
  req.session.userId = userId
2✔
223
  req.session.userName = name
2✔
224
  req.session.userRole = role
2✔
225
  req.session.showWelcome = true
2✔
226

227
  await logActivity(String(userId), ActionTypes.USER_LOGIN, [])
2✔
228

229
  const dashUrl = role === 'lecturer' ? '/lecturer/dashboard?welcome=1' : '/student/dashboard?welcome=1'
2✔
230
  return res.redirect(dashUrl)
2✔
231
}
232

233
const logout = async (req, res) => {
11✔
234
  if (req.session && req.session.userId) {
2!
235
    await logActivity(req.session.userId, ActionTypes.USER_LOGOUT, [])
2✔
236
  }
237

238
  req.session.destroy(() => {
2✔
239
    res.redirect('/')
2✔
240
  })
241
}
242

243
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