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

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

16 May 2026 09:55PM UTC coverage: 89.978%. Remained the same
25973956313

push

github

Sarcastic-Sunflower
Merge branch 'main' of github.com:witseie-elen4010/2026-group-lab-002

666 of 794 branches covered (83.88%)

Branch coverage included in aggregate %.

48 of 65 new or added lines in 15 files covered. (73.85%)

38 existing lines in 9 files now uncovered.

1417 of 1521 relevant lines covered (93.16%)

13.52 hits per line

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

82.88
/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!
UNCOV
107
        req.session.pendingUserId = student.student_number
×
UNCOV
108
        req.session.pendingUserRole = 'student'
×
UNCOV
109
        req.session.pendingUserName = student.name
×
UNCOV
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.userId = student.student_number
45✔
122
        req.session.userName = student.name
45✔
123
        req.session.userRole = 'student'
45✔
124
        req.session.showWelcome = true
45✔
125
        await logActivity(student.student_number, ActionTypes.USER_LOGIN, [])
45✔
126
        const hasEnrollments = db.prepare('SELECT 1 FROM enrollments WHERE student_number = ? LIMIT 1').get(student.student_number)
45✔
127
        if (!hasEnrollments) return res.redirect('/student/courses?onboarding=true')
45✔
128
        return res.redirect('/student/dashboard?welcome=1')
39✔
129
      } 
130
      
UNCOV
131
      const { attempts, pinSent } = await _recordFailedAttempt(student.student_number, 'students', student.email)
×
UNCOV
132
      if (attempts >= 4) {
×
UNCOV
133
        const msg = pinSent
×
134
          ? 'Too many failed attempts. A security PIN has been sent to your email — you will need it to log in.'
135
          : 'Too many failed attempts. A security PIN was already sent to your email. Check your inbox.'
UNCOV
136
        return res.render('login', { error: msg, success: null })
×
137
      }
138
      
139
      const remaining = 4 - attempts
×
140
      return res.render('login', {
×
141
        error: `Invalid password. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining before account lockout.`,
×
142
        success: null
143
      })
144
    }
145

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

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

170
  } catch (error) {
UNCOV
171
    console.error('Login error:', error)
×
UNCOV
172
    return res.render('login', { error: 'An unexpected error occurred. Please try again.', success: null })
×
173
  }
174
}
175

176
const showLoginPin = (req, res) => {
13✔
177
  if (!req.session.pendingUserId) return res.redirect('/login')
2✔
178
  return res.render('login-pin', { error: null, message: null })
1✔
179
}
180

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

184
  const userId = req.session.pendingUserId
1✔
185
  const role = req.session.pendingUserRole
1✔
186
  const table = role === 'lecturer' ? 'staff' : 'students'
1!
187
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
1!
188

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

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

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

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

208
  const { pin } = req.body
3✔
209
  const userId = req.session.pendingUserId
3✔
210
  const role = req.session.pendingUserRole
3✔
211
  const name = req.session.pendingUserName
3✔
212

213
  const table = role === 'lecturer' ? 'staff' : 'students'
3✔
214
  const idCol = role === 'lecturer' ? 'staff_number' : 'student_number'
3✔
215

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

218
  if (!user || !user.login_pin) {
3!
UNCOV
219
    delete req.session.pendingUserId
×
UNCOV
220
    return res.redirect('/login')
×
221
  }
222

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

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

230
  delete req.session.pendingUserId
2✔
231
  delete req.session.pendingUserRole
2✔
232
  delete req.session.pendingUserName
2✔
233
  req.session.userId = userId
2✔
234
  req.session.userName = name
2✔
235
  req.session.userRole = role
2✔
236
  req.session.showWelcome = true
2✔
237

238
  await logActivity(String(userId), ActionTypes.USER_LOGIN, [])
2✔
239

240
  const dashUrl = role === 'lecturer' ? '/lecturer/dashboard?welcome=1' : '/student/dashboard?welcome=1'
2✔
241
  return res.redirect(dashUrl)
2✔
242
}
243

244
const logout = async (req, res) => {
13✔
245
  if (req.session && req.session.userId) {
3!
246
    await logActivity(req.session.userId, ActionTypes.USER_LOGOUT, [])
3✔
247
  }
248

249
  req.session.destroy((err) => {
3✔
250
  if (err) {
3!
NEW
251
    console.error('Session destruction error:', err);
×
252
  }
253

254
  res.clearCookie('connect.sid');
3✔
255
  res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
3✔
256
  res.set('Pragma', 'no-cache');
3✔
257
  res.set('Expires', '0');
3✔
258
  res.redirect('/');
3✔
259
});
260
}
261

262
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