• 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

84.57
/src/controllers/admin-controller.js
1
const db = require('../../database/db')
13✔
2
const { logActivity } = require('../services/logging-service')
13✔
3
const ActionTypes = require('../services/action-types')
13✔
4
const { FK_DISPLAY, getInputType, friendlyError, buildSearchQuery } = require('../services/admin-helpers')
13✔
5
const { logAdminAudit } = require('../services/admin-audit-service')
13✔
6
const { getFailedLoginCount } = require('./admin-activity-log-controller')
13✔
7

8
const PAGE_SIZE = 20
13✔
9
// tables the admin UI can view but not modify
10
const READ_ONLY_TABLES = ['admin_audit_log', 'activity_log', 'affected_records', 'actions', 'failed_login_log']
13✔
11

12
const getAllTables = () =>
13✔
13
  db.prepare('SELECT name FROM sqlite_master WHERE type=\'table\' ORDER BY name').all().map(r => r.name)
229✔
14

15
const getColumns = (tableName) =>
13✔
16
  db.prepare(`PRAGMA table_info("${tableName}")`).all()
11✔
17

18
const getForeignKeys = (tableName) =>
13✔
19
  db.prepare(`PRAGMA foreign_key_list("${tableName}")`).all()
5✔
20

21
const getForeignKeyOptions = (fkList) => {
13✔
22
  const options = {}
5✔
23
  fkList.forEach(fk => {
5✔
24
    if (!options[fk.from]) {
1!
25
      const display = FK_DISPLAY[fk.table]
1✔
26
      if (display) {
1!
27
        const [valCol, labelCol] = display
×
28
        const rows = db.prepare(`SELECT "${valCol}" as val, "${labelCol}" as label FROM "${fk.table}" ORDER BY 2`).all()
×
29
        options[fk.from] = rows.map(r => ({ value: r.val, label: `${r.val} — ${r.label}` }))
×
30
      } else {
31
        const rows = db.prepare(`SELECT "${fk.to}" as val FROM "${fk.table}" ORDER BY 1`).all()
1✔
32
        options[fk.from] = rows.map(r => ({ value: r.val, label: String(r.val) }))
18✔
33
      }
34
    }
35
  })
36
  return options
5✔
37
}
38

39
const getInputTypes = (columns) => {
13✔
40
  const types = {}
5✔
41
  columns.forEach(col => { types[col.name] = getInputType(col.name) })
12✔
42
  return types
5✔
43
}
44

45
const showAdminDashboard = (req, res) => {
13✔
46
  if (!req.session || !req.session.userId) {
1!
NEW
47
    return res.redirect('/login');
×
48
  }
49
  const user = { id: req.session.userId, name: req.session.userName }
1✔
50
  const tables = getAllTables()
1✔
51
  const stats = {
1✔
52
    students: db.prepare('SELECT COUNT(*) as n FROM students').get().n,
53
    staff: db.prepare('SELECT COUNT(*) as n FROM staff').get().n,
54
    consultations: db.prepare('SELECT COUNT(*) as n FROM consultations').get().n,
55
    availability: db.prepare('SELECT COUNT(*) as n FROM lecturer_availability').get().n
56
  }
57
  res.render('admin-dashboard', {
1✔
58
    user,
59
    tables,
60
    stats,
61
    activeTable: null,
62
    isReadOnly: false,
63
    columns: [],
64
    rows: [],
65
    page: 1,
66
    totalPages: 1,
67
    totalRows: 0,
68
    search: '',
69
    fkOptions: {},
70
    inputTypes: {},
71
    failedLoginCount: getFailedLoginCount(),
72
    error: null,
73
    success: null
74
  })
75
}
76

77
const showTable = (req, res) => {
13✔
78
  const user = { id: req.session.userId, name: req.session.userName }
4✔
79
  const tables = getAllTables()
4✔
80
  const { tableName } = req.params
4✔
81

82
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
4✔
83

84
  const page = Math.max(1, parseInt(req.query.page) || 1)
3✔
85
  const offset = (page - 1) * PAGE_SIZE
3✔
86
  const search = (req.query.search || '').trim()
3✔
87
  const columns = getColumns(tableName)
3✔
88
  const fkOptions = getForeignKeyOptions(getForeignKeys(tableName))
3✔
89
  const inputTypes = getInputTypes(columns)
3✔
90

91
  let totalRows, rows
92
  if (tableName === 'consultations') {
3!
93
    const consultFrom = `
×
94
      FROM consultations c
95
      LEFT JOIN staff     stf ON c.lecturer_id = stf.staff_number
96
      LEFT JOIN students  st  ON c.organiser   = st.student_number
97
    `
98
    const courseInfo = `(
×
99
      SELECT cr.course_code || ' — ' || cr.course_name
100
      FROM courses cr
101
      JOIN staff_courses sc ON sc.course_code = cr.course_code
102
      JOIN enrollments   e  ON e.course_code  = cr.course_code
103
      WHERE sc.staff_number = c.lecturer_id AND e.student_number = c.organiser
104
      LIMIT 1
105
    ) AS course_info`
106
    const select = `SELECT c.*, c.rowid, stf.name AS lecturer_name, st.name AS organiser_name, ${courseInfo}`
×
107
    if (search) {
×
108
      const like = `%${search}%`
×
109
      const searchWhere = `WHERE (c.consultation_title LIKE ? OR c.consultation_date LIKE ?
×
110
        OR c.lecturer_id LIKE ? OR CAST(c.organiser AS TEXT) LIKE ? OR c.status LIKE ?)`
111
      const sp = [like, like, like, like, like]
×
112
      totalRows = db.prepare(`SELECT COUNT(*) as count ${consultFrom} ${searchWhere}`).get(...sp).count
×
113
      rows = db.prepare(`${select} ${consultFrom} ${searchWhere} ORDER BY c.consultation_date DESC LIMIT ? OFFSET ?`).all(...sp, PAGE_SIZE, offset)
×
114
    } else {
115
      totalRows = db.prepare(`SELECT COUNT(*) as count FROM consultations`).get().count
×
116
      rows = db.prepare(`${select} ${consultFrom} ORDER BY c.consultation_date DESC LIMIT ? OFFSET ?`).all(PAGE_SIZE, offset)
×
117
    }
118
  } else if (search) {
3✔
119
    const { whereClauses, params } = buildSearchQuery(columns, search)
1✔
120
    totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}" WHERE ${whereClauses}`).get(...params).count
1✔
121
    rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" WHERE ${whereClauses} LIMIT ? OFFSET ?`).all(...params, PAGE_SIZE, offset)
1✔
122
  } else {
123
    totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get().count
2✔
124
    rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" LIMIT ? OFFSET ?`).all(PAGE_SIZE, offset)
2✔
125
  }
126

127
  const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE))
3✔
128

129
  res.render('admin-dashboard', {
3✔
130
    user,
131
    tables,
132
    activeTable: tableName,
133
    isReadOnly: READ_ONLY_TABLES.includes(tableName),
134
    columns,
135
    rows,
136
    page,
137
    totalPages,
138
    totalRows,
139
    search,
140
    fkOptions,
141
    inputTypes,
142
    failedLoginCount: getFailedLoginCount(),
143
    error: req.query.error || null,
6✔
144
    success: req.query.success || null
6✔
145
  })
146
}
147

148
const createRecord = async (req, res) => {
13✔
149
  const tables = getAllTables()
11✔
150
  const { tableName } = req.params
11✔
151
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
11!
152
  if (READ_ONLY_TABLES.includes(tableName))
11✔
153
    return res.redirect(`/admin/table/${tableName}?error=This+table+is+read-only`)
7✔
154

155
  const columns = getColumns(tableName)
4✔
156
  const fields = columns.map(c => c.name)
8✔
157
  const values = fields.map(f => (req.body[f] !== '' && req.body[f] !== undefined) ? req.body[f] : null)
8✔
158
  const placeholders = fields.map(() => '?').join(', ')
8✔
159
  const fieldList = fields.map(f => `"${f}"`).join(', ')
8✔
160

161
  try {
4✔
162
    const result = db.prepare(`INSERT INTO "${tableName}" (${fieldList}) VALUES (${placeholders})`).run(...values)
4✔
163
    logAdminAudit({
2✔
164
      adminId: req.session.userId,
165
      action: 'INSERT',
166
      tableName,
167
      rowId: result.lastInsertRowid,
168
      newData: req.body
169
    })
170
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_ADD, [{ table: tableName, id: result.lastInsertRowid }])
2✔
171
    res.redirect(`/admin/table/${tableName}?success=Record+added`)
2✔
172
  } catch (err) {
173
    const fkOptions = getForeignKeyOptions(getForeignKeys(tableName))
2✔
174
    const inputTypes = getInputTypes(columns)
2✔
175
    const totalRows = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get().count
2✔
176
    const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE))
2✔
177
    const rows = db.prepare(`SELECT *, rowid as rowid FROM "${tableName}" LIMIT ?`).all(PAGE_SIZE)
2✔
178
    res.render('admin-dashboard', {
2✔
179
      user: { id: req.session.userId, name: req.session.userName },
180
      tables,
181
      activeTable: tableName,
182
      columns,
183
      rows,
184
      page: 1,
185
      totalPages,
186
      totalRows,
187
      search: '',
188
      fkOptions,
189
      inputTypes,
190
      error: friendlyError(err.message),
191
      success: null
192
    })
193
  }
194
}
195

196
const updateRecord = async (req, res) => {
13✔
197
  const tables = getAllTables()
5✔
198
  const { tableName, rowId } = req.params
5✔
199
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
5!
200
  if (READ_ONLY_TABLES.includes(tableName))
5✔
201
    return res.redirect(`/admin/table/${tableName}?error=This+table+is+read-only`)
1✔
202

203
  const columns = getColumns(tableName)
4✔
204
  const updatable = columns.filter(c => c.pk === 0)
8✔
205
  if (updatable.length === 0) { return res.redirect(`/admin/table/${tableName}?error=This+table+has+no+editable+columns`) }
4!
206

207
  const existingRecord = db.prepare(`SELECT *, rowid FROM "${tableName}" WHERE rowid = ?`).get(rowId)
4✔
208
  if (!existingRecord)
4✔
209
    return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
1✔
210

211
  const setClauses = updatable.map(c => `"${c.name}" = ?`).join(', ')
3✔
212
  const values = [
3✔
213
    ...updatable.map(c => (req.body[c.name] !== '' && req.body[c.name] !== undefined) ? req.body[c.name] : null),
3✔
214
    rowId
215
  ]
216

217
  try {
3✔
218
    const result = db.prepare(`UPDATE "${tableName}" SET ${setClauses} WHERE rowid = ?`).run(...values)
3✔
219
    if (result.changes === 0)
2!
220
      return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
×
221
    logAdminAudit({
2✔
222
      adminId: req.session.userId,
223
      action: 'UPDATE',
224
      tableName,
225
      rowId,
226
      oldData: existingRecord,
227
      newData: req.body
228
    })
229
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_EDIT, [{ table: tableName, id: rowId }])
2✔
230
    res.redirect(`/admin/table/${tableName}?success=Record+updated`)
2✔
231
  } catch (err) {
232
    res.redirect(`/admin/table/${tableName}?error=${encodeURIComponent(friendlyError(err.message))}`)
1✔
233
  }
234
}
235

236
const deleteRecord = async (req, res) => {
13✔
237
  const tables = getAllTables()
6✔
238
  const { tableName, rowId } = req.params
6✔
239
  if (!tables.includes(tableName)) return res.status(404).send('Table not found')
6!
240
  if (tableName === 'admins') return res.redirect('/admin/table/admins?error=Admin+accounts+cannot+be+deleted')
6✔
241
  if (READ_ONLY_TABLES.includes(tableName))
5✔
242
    return res.redirect(`/admin/table/${tableName}?error=Audit+log+entries+cannot+be+deleted`)
1✔
243

244
  const existingRecord = db.prepare(`SELECT *, rowid FROM "${tableName}" WHERE rowid = ?`).get(rowId)
4✔
245
  if (!existingRecord)
4✔
246
    return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
1✔
247

248
  try {
3✔
249
    const result = db.prepare(`DELETE FROM "${tableName}" WHERE rowid = ?`).run(rowId)
3✔
250
    if (result.changes === 0)
2!
251
      return res.redirect(`/admin/table/${tableName}?error=Record+not+found`)
×
252
    logAdminAudit({
2✔
253
      adminId: req.session.userId,
254
      action: 'DELETE',
255
      tableName,
256
      rowId,
257
      oldData: existingRecord
258
    })
259
    await logActivity(req.session.userId, ActionTypes.ADMIN_USER_DELETE, [{ table: tableName, id: rowId }])
2✔
260
    res.redirect(`/admin/table/${tableName}?success=Record+deleted`)
2✔
261
  } catch (err) {
262
    res.redirect(`/admin/table/${tableName}?error=${encodeURIComponent(friendlyError(err.message))}`)
1✔
263
  }
264
}
265

266
module.exports = { showAdminDashboard, showTable, createRecord, updateRecord, deleteRecord }
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