Implement user authentication and role-based access control
Three-tier user model: admin (all accounts, all actions), editor (assigned accounts, read/write), viewer (assigned accounts, read-only). Backend: - express-session with custom SQLite session store (no extra packages) - bcryptjs for password hashing - src/middleware/auth.js: requireAuth, requireAdmin, requireEditor, canAccessAccount helpers - src/routes/auth.js: login, logout, /me, setup-needed, change-password - src/routes/users.js: full CRUD + account assignments (admin only) - All API routes protected; /api/accounts filtered by user access; write routes gated by requireEditor; admin-only routes locked down Frontend: - Login overlay (full-page) with first-run admin-setup flow - Role-based UI: admin-only elements hidden for non-admins; edit/delete and PDF buttons hidden for viewers; account switcher shows only accessible accounts for non-admins - Users modal (admin only): user list with role badges, create/edit/delete users, set account access via checkboxes - Change-password section available to all logged-in users - apiFetch redirects to login on 401
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const { Store } = require('express-session');
|
||||
|
||||
// SQLite-backed session store using the existing better-sqlite3 db instance.
|
||||
// No additional npm packages required.
|
||||
class SessionStore extends Store {
|
||||
constructor(db) {
|
||||
super();
|
||||
this.db = db;
|
||||
// Purge expired sessions every 10 minutes
|
||||
setInterval(() => {
|
||||
try { db.prepare('DELETE FROM sessions WHERE expired < ?').run(Date.now()); } catch (_) {}
|
||||
}, 10 * 60 * 1000).unref();
|
||||
}
|
||||
|
||||
get(sid, cb) {
|
||||
try {
|
||||
const row = this.db.prepare('SELECT sess, expired FROM sessions WHERE sid = ?').get(sid);
|
||||
if (!row) return cb(null, null);
|
||||
if (Date.now() > row.expired) {
|
||||
this.destroy(sid, () => {});
|
||||
return cb(null, null);
|
||||
}
|
||||
cb(null, JSON.parse(row.sess));
|
||||
} catch (e) { cb(e); }
|
||||
}
|
||||
|
||||
set(sid, sess, cb) {
|
||||
try {
|
||||
const maxAge = (sess.cookie && sess.cookie.maxAge)
|
||||
? sess.cookie.maxAge * 1000
|
||||
: 7 * 24 * 60 * 60 * 1000;
|
||||
const expired = Date.now() + maxAge;
|
||||
this.db.prepare(
|
||||
'INSERT OR REPLACE INTO sessions (sid, sess, expired) VALUES (?, ?, ?)'
|
||||
).run(sid, JSON.stringify(sess), expired);
|
||||
cb(null);
|
||||
} catch (e) { cb(e); }
|
||||
}
|
||||
|
||||
destroy(sid, cb) {
|
||||
try {
|
||||
this.db.prepare('DELETE FROM sessions WHERE sid = ?').run(sid);
|
||||
cb(null);
|
||||
} catch (e) { cb(e); }
|
||||
}
|
||||
|
||||
touch(sid, sess, cb) {
|
||||
this.set(sid, sess, cb);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionStore;
|
||||
Reference in New Issue
Block a user