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

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

16 May 2026 09:36PM UTC coverage: 89.845% (-1.3%) from 91.142%
25973568317

push

github

web-flow
Merge pull request #126 from witseie-elen4010/session_security

Session security

665 of 794 branches covered (83.75%)

Branch coverage included in aggregate %.

56 of 76 new or added lines in 17 files covered. (73.68%)

2 existing lines in 1 file now uncovered.

1423 of 1530 relevant lines covered (93.01%)

13.53 hits per line

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

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

8
const showLogin = (req, res) => {
13✔
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) => {
13✔
19
  const idCol = table === 'staff' ? 'staff_number' : 'student_number'
3!
20

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

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

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

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

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

49
    if (!staffStudentNumber || !password) {
85!
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)
85✔
55
    if (staff) {
85✔
56
      if (staff.login_pin) {
24✔
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)
23✔
64

65
      if (isMatch) {
23✔
66
        if (!staff.email_verified) {
20✔
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)
19✔
71
        req.session.regenerate((err) => {
19✔
72
        if (err) {
19!
NEW
73
          return res.status(500).render('login', {
×
74
            error: 'Session error'
75
          })
76
        }})
77

78
        req.session.userId = staff.staff_number
19✔
79
        req.session.userName = staff.name
19✔
80
        req.session.userRole = 'lecturer'
19✔
81
        req.session.showWelcome = true
19✔
82
        await logActivity(staff.staff_number, ActionTypes.USER_LOGIN, [])
19✔
83
        const hasLecturerCourses = db.prepare('SELECT 1 FROM staff_courses WHERE staff_number = ? LIMIT 1').get(staff.staff_number)
19✔
84
        if (!hasLecturerCourses) return res.redirect('/lecturer/courses/edit?onboarding=true')
19✔
85
        return res.redirect('/lecturer/dashboard?welcome=1')
13✔
86
      }
87

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

103
    // 2. Student Check
104
    const student = db.prepare('SELECT * FROM students WHERE student_number = ?').get(staffStudentNumber)
61✔
105
    if (student) {
61✔
106
      if (student.login_pin) {
47!
107
        req.session.pendingUserId = student.student_number
×
108
        req.session.pendingUserRole = 'student'
×
109
        req.session.pendingUserName = student.name
×
110
        return res.redirect('/login/pin')
×
111
      }
112

113
      const isMatch = await bcryptjs.compare(password, student.password)
47✔
114
      
115
      if (isMatch) {
47!
116
        if (!student.email_verified) {
47✔
117
          return res.redirect(`/verify-email?email=${encodeURIComponent(student.email)}&fromLogin=1`)
2✔
118
        }
119
        
120
        db.prepare('UPDATE students SET failed_attempts = 0 WHERE student_number = ?').run(student.student_number)
45✔
121
        req.session.regenerate((err) => {
45✔
122
        if (err) {
45!
NEW
123
          return res.status(500).render('login', {
×
124
            error: 'Session error'
125
          })
126
        }})
127
        
128

129
        req.session.userId = student.student_number
45✔
130
        req.session.userName = student.name
45✔
131
        req.session.userRole = 'student'
45✔
132
        req.session.showWelcome = true
45✔
133
        await logActivity(student.student_number, ActionTypes.USER_LOGIN, [])
45✔
134
        const hasEnrollments = db.prepare('SELECT 1 FROM enrollments WHERE student_number = ? LIMIT 1').get(student.student_number)
45✔
135
        if (!hasEnrollments) return res.redirect('/student/courses?onboarding=true')
45✔
136
        return res.redirect('/student/dashboard?welcome=1')
39✔
137
      } 
138
      
139
      const { attempts, pinSent } = await _recordFailedAttempt(student.student_number, 'students', student.email)
×
140
      if (attempts >= 4) {
×
141
        const msg = pinSent
×
142
          ? 'Too many failed attempts. A security PIN has been sent to your email — you will need it to log in.'
143
          : 'Too many failed attempts. A security PIN was already sent to your email. Check your inbox.'
144
        return res.render('login', { error: msg, success: null })
×
145
      }
146
      
147
      const remaining = 4 - attempts
×
148
      return res.render('login', {
×
149
        error: `Invalid password. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining before account lockout.`,
×
150
        success: null
151
      })
152
    }
153

154
    // 3. Admin Check
155
    let admin = null
14✔
156
    try { 
14✔
157
      admin = db.prepare('SELECT * FROM admins WHERE admin_id = ?').get(staffStudentNumber) 
14✔
158
    } catch (_) {} 
159
    
160
    if (admin) {
14✔
161
      const isMatch = await bcryptjs.compare(password, admin.password)
13✔
162
      if (isMatch) {
13!
163
        req.session.userId = admin.admin_id
13✔
164
        req.session.userName = admin.name
13✔
165
        req.session.userRole = 'admin'
13✔
166
        await logActivity(admin.admin_id, ActionTypes.ADMIN_LOGIN, [])
13✔
167
        return res.redirect('/admin/dashboard')
13✔
168
      } else { 
169
        await logActivity(admin.admin_id, ActionTypes.AUTH_FAILED_LOGIN, [])
×
170
        return res.render('login', { error: 'Invalid password.', success: null })
×
171
      }
172
    } 
173

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

178
  } catch (error) {
179
    console.error('Login error:', error)
×
180
    return res.render('login', { error: 'An unexpected error occurred. Please try again.', success: null })
×
181
  }
182
}
183

184
const showLoginPin = (req, res) => {
13✔
185
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
186
  return res.render('login-pin', { error: null, message: null })
1✔
187
}
188

189
const resendLoginPin = async (req, res) => {
13✔
190
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
191

192
  const userId = req.session.pendingUserId
1✔
193
  const role = req.session.pendingUserRole
1✔
194
  const table = role === 'lecturer' ? 'staff' : 'students'
1!
195
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
1!
196

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

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

204
  try {
1✔
205
    await sendLoginWarningEmail(user.email, pin)
1✔
206
    return res.render('login-pin', { error: null, message: 'A new PIN has been sent to your email.' })
1✔
207
  } catch (err) {
208
    console.error('Resend PIN email failed:', err)
×
209
    return res.render('login-pin', { error: 'Failed to resend PIN. Please try again.', message: null })
×
210
  }
211
}
212

213
const verifyLoginPin = async (req, res) => {
13✔
214
  if (!req.session.pendingUserId) return res.redirect('/login')
4✔
215

216
  const { pin } = req.body
3✔
217
  const userId = req.session.pendingUserId
3✔
218
  const role = req.session.pendingUserRole
3✔
219
  const name = req.session.pendingUserName
3✔
220

221
  const table = role === 'lecturer' ? 'staff' : 'students'
3✔
222
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
3✔
223

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

226
  if (!user || !user.login_pin) {
3!
227
    delete req.session.pendingUserId
×
228
    return res.redirect('/login')
×
229
  }
230

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

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

238
  delete req.session.pendingUserId
2✔
239
  delete req.session.pendingUserRole
2✔
240
  delete req.session.pendingUserName
2✔
241
  req.session.userId = userId
2✔
242
  req.session.userName = name
2✔
243
  req.session.userRole = role
2✔
244
  req.session.showWelcome = true
2✔
245

246
  await logActivity(String(userId), ActionTypes.USER_LOGIN, [])
2✔
247

248
  const dashUrl = role === 'lecturer' ? '/lecturer/dashboard?welcome=1' : '/student/dashboard?welcome=1'
2✔
249
  return res.redirect(dashUrl)
2✔
250
}
251

252
const logout = async (req, res) => {
13✔
253
  if (req.session && req.session.userId) {
3!
254
    await logActivity(req.session.userId, ActionTypes.USER_LOGOUT, [])
3✔
255
  }
256

257
  req.session.destroy((err) => {
3✔
258
  if (err) {
3!
NEW
259
    console.error('Session destruction error:', err);
×
260
  }
261

262
  res.clearCookie('connect.sid');
3✔
263
  res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
3✔
264
  res.set('Pragma', 'no-cache');
3✔
265
  res.set('Expires', '0');
3✔
266
  res.redirect('/');
3✔
267
});
268
}
269

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