Fix RBAC, user creation and update components
This commit is contained in:
+60
-35
@@ -167,7 +167,9 @@ app.post('/auth/login', async (req, res) => {
|
||||
if (!email || !password) return res.status(400).json({ error: 'email and password required' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, email, password_hash, full_name, is_active FROM auth.users WHERE email = $1 LIMIT 1',
|
||||
`SELECT id, email, password_hash, full_name, is_active,
|
||||
coalesce((to_jsonb(u)->>'is_system_admin')::boolean, false) AS is_system_admin
|
||||
FROM auth.users u WHERE email = $1 LIMIT 1`,
|
||||
[String(email).trim().toLowerCase()],
|
||||
);
|
||||
const u = rows[0];
|
||||
@@ -176,7 +178,7 @@ app.post('/auth/login', async (req, res) => {
|
||||
if (!ok) return res.status(401).json({ error: 'invalid credentials' });
|
||||
await pool.query('UPDATE auth.users SET last_login_at = now() WHERE id = $1', [u.id]);
|
||||
const token = signToken(u);
|
||||
res.json({ token, user: { id: u.id, email: u.email, full_name: u.full_name } });
|
||||
res.json({ token, user: { id: u.id, email: u.email, full_name: u.full_name, is_system_admin: u.is_system_admin } });
|
||||
} catch (e) { dbError(res, e); }
|
||||
});
|
||||
|
||||
@@ -221,7 +223,8 @@ app.post('/rpc/:fn', authRequired, async (req, res) => {
|
||||
// GET /from/:view?col=val&col2=val2 -> SELECT * FROM app.<view> WHERE ...
|
||||
const ALLOWED_VIEWS = new Set([
|
||||
'v_my_tills', 'v_my_shops', 'v_services_active', 'v_my_recent_transactions',
|
||||
'v_manage_tills',
|
||||
'v_manage_tills', 'v_service_ui_settings',
|
||||
'v_employee_outstanding_balances',
|
||||
// Owner / manager dashboards (RLS still restricts rows to allowed shops):
|
||||
'v_owner_dashboard', 'v_z_report', 'v_employee_scorecard_30d', 'alerts', 'v_end_of_day_reports',
|
||||
]);
|
||||
@@ -311,30 +314,66 @@ app.post('/employee_transactions', authRequired, async (req, res) => {
|
||||
// ---- admin: user management -----------------------------------------
|
||||
|
||||
async function ensureAdmin(req, res) {
|
||||
// Owner-anywhere == admin in the UI. Compute via app.is_owner_anywhere().
|
||||
try {
|
||||
const ok = await withUserClient(req, async (client) => {
|
||||
const r = await client.query('SELECT app.is_owner_anywhere() AS ok');
|
||||
return !!r.rows[0]?.ok;
|
||||
});
|
||||
if (!ok) { res.status(403).json({ error: 'admin only' }); return false; }
|
||||
const { rows } = await pool.query(`
|
||||
SELECT coalesce((to_jsonb(u)->>'is_system_admin')::boolean, false) AS ok
|
||||
FROM auth.users u
|
||||
WHERE u.id = $1
|
||||
`, [req.user.id]);
|
||||
const ok = !!rows[0]?.ok;
|
||||
if (!ok) {
|
||||
res.status(403).json({ error: 'admin only' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) { dbError(res, e); return false; }
|
||||
}
|
||||
|
||||
function normalizeUserRole(role) {
|
||||
if (role === 'admin' || role === 'owner' || role === 'employee') return role;
|
||||
return 'employee';
|
||||
}
|
||||
|
||||
async function firstShopId(client) {
|
||||
const shop = await client.query('SELECT id FROM app.shops ORDER BY created_at LIMIT 1');
|
||||
return shop.rows[0]?.id ?? null;
|
||||
}
|
||||
|
||||
async function assignShopRole(client, userId, role, assignedBy, shopIdParam) {
|
||||
const shopId = shopIdParam || await firstShopId(client);
|
||||
if (!shopId) return;
|
||||
await client.query(
|
||||
`INSERT INTO app.user_shop_assignments(user_id, shop_id, role, assigned_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, shop_id) DO UPDATE SET role = EXCLUDED.role`,
|
||||
[userId, shopId, role, assignedBy],
|
||||
);
|
||||
}
|
||||
|
||||
async function upsertEmployee(client, { empId, name, email, department }) {
|
||||
if (!empId) return;
|
||||
await client.query(
|
||||
`INSERT INTO app.employees(emp_id, name, email, department)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (emp_id) DO UPDATE
|
||||
SET name = EXCLUDED.name, email = EXCLUDED.email,
|
||||
department = EXCLUDED.department`,
|
||||
[empId, name, email, department || null],
|
||||
);
|
||||
}
|
||||
|
||||
app.get('/admin/users', authRequired, async (req, res) => {
|
||||
if (!(await ensureAdmin(req, res))) return;
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT u.id, u.email, u.is_active, u.full_name,
|
||||
p.full_name AS profile_name,
|
||||
coalesce((to_jsonb(u)->>'is_system_admin')::boolean, false) AS is_system_admin,
|
||||
coalesce(
|
||||
(SELECT a.role::text FROM app.user_shop_assignments a
|
||||
WHERE a.user_id = u.id ORDER BY (a.role = 'owner') DESC LIMIT 1),
|
||||
'cashier'
|
||||
) AS shop_role,
|
||||
EXISTS (SELECT 1 FROM app.user_shop_assignments a
|
||||
WHERE a.user_id = u.id AND a.role = 'owner') AS is_admin,
|
||||
(SELECT e.emp_id FROM app.employees e WHERE e.email = u.email LIMIT 1) AS emp_id,
|
||||
(SELECT e.department FROM app.employees e WHERE e.email = u.email LIMIT 1) AS department
|
||||
FROM auth.users u
|
||||
@@ -347,23 +386,25 @@ app.get('/admin/users', authRequired, async (req, res) => {
|
||||
|
||||
app.post('/admin/users', authRequired, async (req, res) => {
|
||||
if (!(await ensureAdmin(req, res))) return;
|
||||
const { email, password, name, role, department, empId } = req.body || {};
|
||||
const { email, password, name, role, department, empId, shopId } = req.body || {};
|
||||
if (!email || !password || !name) {
|
||||
return res.status(400).json({ error: 'email, password, name required' });
|
||||
}
|
||||
if (String(password).length < 6) {
|
||||
return res.status(400).json({ error: 'password must be at least 6 chars' });
|
||||
}
|
||||
const isAdmin = role === 'admin';
|
||||
const userRole = normalizeUserRole(role);
|
||||
const isAdmin = userRole === 'admin';
|
||||
const shopRole = userRole === 'employee' ? 'cashier' : 'owner';
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
const u = await client.query(
|
||||
`INSERT INTO auth.users(email, password_hash, full_name, is_active)
|
||||
VALUES ($1, $2, $3, true) RETURNING id, email, full_name`,
|
||||
[String(email).trim().toLowerCase(), hash, name],
|
||||
`INSERT INTO auth.users(email, password_hash, full_name, is_system_admin, is_active)
|
||||
VALUES ($1, $2, $3, $4, true) RETURNING id, email, full_name`,
|
||||
[String(email).trim().toLowerCase(), hash, name, isAdmin],
|
||||
);
|
||||
const userId = u.rows[0].id;
|
||||
await client.query(
|
||||
@@ -372,25 +413,9 @@ app.post('/admin/users', authRequired, async (req, res) => {
|
||||
ON CONFLICT (user_id) DO UPDATE SET full_name = EXCLUDED.full_name`,
|
||||
[userId, name],
|
||||
);
|
||||
// Assign to the first available shop (Default Shop typically) so they have a role.
|
||||
const shop = await client.query('SELECT id FROM app.shops ORDER BY created_at LIMIT 1');
|
||||
if (shop.rows[0]) {
|
||||
await client.query(
|
||||
`INSERT INTO app.user_shop_assignments(user_id, shop_id, role, assigned_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, shop_id) DO UPDATE SET role = EXCLUDED.role`,
|
||||
[userId, shop.rows[0].id, isAdmin ? 'owner' : 'cashier', req.user.id],
|
||||
);
|
||||
}
|
||||
if (!isAdmin && empId) {
|
||||
await client.query(
|
||||
`INSERT INTO app.employees(emp_id, name, email, department)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (emp_id) DO UPDATE
|
||||
SET name = EXCLUDED.name, email = EXCLUDED.email,
|
||||
department = EXCLUDED.department`,
|
||||
[empId, name, email, department || null],
|
||||
);
|
||||
if (!isAdmin) {
|
||||
await assignShopRole(client, userId, shopRole, req.user.id, shopId);
|
||||
await upsertEmployee(client, { empId, name, email, department });
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
res.json({ data: { id: userId, email: u.rows[0].email, full_name: u.rows[0].full_name } });
|
||||
|
||||
Reference in New Issue
Block a user