added super admin restrictions for site settings

This commit is contained in:
2ManyProjects 2025-05-10 00:01:13 -05:00
parent c35adf0498
commit b02bbb2086
11 changed files with 130 additions and 57 deletions

View file

@ -15,8 +15,24 @@ module.exports = (pool, query) => {
try { try {
// Verify API key and check admin status // Verify API key and check admin status
// const result = await query(
// 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE api_key = $1',
// [apiKey]
// );
const result = await query( const result = await query(
'SELECT id, email, first_name, last_name, is_admin FROM users WHERE api_key = $1', `SELECT
u.id,
u.email,
u.first_name,
u.last_name,
u.is_admin,
CASE WHEN s.user_id IS NOT NULL THEN TRUE ELSE FALSE END AS is_super_admin
FROM
users u
LEFT JOIN
superadmins s ON u.id = s.user_id
WHERE
u.api_key = $1`,
[apiKey] [apiKey]
); );

View file

@ -42,6 +42,10 @@ module.exports = (pool, query) => {
'UPDATE users SET is_admin = TRUE WHERE email = $1', 'UPDATE users SET is_admin = TRUE WHERE email = $1',
[email] [email]
); );
await query(
'INSERT INTO superadmins (user_id) values ($1) RETURNING id, created_at',
[result.rows[0].id]
);
} }
await emailService.sendWelcomeEmail({ await emailService.sendWelcomeEmail({
@ -205,15 +209,33 @@ module.exports = (pool, query) => {
); );
// Get user information including admin status // Get user information including admin status
// const userInfo = await query(
// 'SELECT id, email, first_name, last_name, is_admin FROM users WHERE id = $1',
// [userId]
// );
const userInfo = await query( const userInfo = await query(
'SELECT id, email, first_name, last_name, is_admin FROM users WHERE id = $1', `SELECT
u.id,
u.email,
u.first_name,
u.last_name,
u.is_admin,
CASE WHEN s.user_id IS NOT NULL THEN TRUE ELSE FALSE END AS is_super_admin
FROM
users u
LEFT JOIN
superadmins s ON u.id = s.user_id
WHERE
u.id = $1`,
[userId] [userId]
); );
res.json({ res.json({
message: 'Login successful', message: 'Login successful',
userId: userId, userId: userId,
isAdmin: userInfo.rows[0].is_admin, isAdmin: userInfo.rows[0].is_admin,
isSuperAdmin: userInfo.rows[0].is_super_admin,
firstName: userInfo.rows[0].first_name, firstName: userInfo.rows[0].first_name,
lastName: userInfo.rows[0].last_name, lastName: userInfo.rows[0].last_name,
email: userInfo.rows[0].email, email: userInfo.rows[0].email,

View file

@ -113,7 +113,14 @@ module.exports = (pool, query, authMiddleware) => {
message: 'Value and category are required' message: 'Value and category are required'
}); });
} }
const setting = await SystemSettings.getSetting(pool, query, key)
if(setting?.super_req && !req.user.is_super_admin){
return res.status(400).json({
error: true,
message: `Super Admin access required to modify ${key}`
});
}
const updatedSetting = await SystemSettings.updateSetting(pool, query, key, value, category); const updatedSetting = await SystemSettings.updateSetting(pool, query, key, value, category);
// Update config in memory // Update config in memory
@ -137,7 +144,7 @@ module.exports = (pool, query, authMiddleware) => {
router.post('/batch', async (req, res, next) => { router.post('/batch', async (req, res, next) => {
try { try {
const { settings } = req.body; const { settings } = req.body;
console.log(req.user)
if (!req.user.is_admin) { if (!req.user.is_admin) {
return res.status(403).json({ return res.status(403).json({
@ -152,7 +159,7 @@ module.exports = (pool, query, authMiddleware) => {
message: 'Settings array is required' message: 'Settings array is required'
}); });
} }
const categorySettings = await SystemSettings.getSettingsByCategory(pool, query, settings[0]?.category)
// Validate all settings have required fields // Validate all settings have required fields
for (const setting of settings) { for (const setting of settings) {
if (!setting.key || setting.value === undefined || !setting.category) { if (!setting.key || setting.value === undefined || !setting.category) {
@ -161,6 +168,12 @@ module.exports = (pool, query, authMiddleware) => {
message: 'Each setting must have key, value, and category fields' message: 'Each setting must have key, value, and category fields'
}); });
} }
if(categorySettings.find(item => item.key === setting.key)?.super_req && !req.user.is_super_admin){
return res.status(400).json({
error: true,
message: `Super Admin access required to modify ${setting.key}`
});
}
} }
const updatedSettings = await SystemSettings.updateSettings(pool, query, settings); const updatedSettings = await SystemSettings.updateSettings(pool, query, settings);

View file

@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS system_settings (
key VARCHAR(255) NOT NULL UNIQUE, key VARCHAR(255) NOT NULL UNIQUE,
value TEXT, value TEXT,
category VARCHAR(100) NOT NULL, category VARCHAR(100) NOT NULL,
super_req BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
); );
@ -19,51 +20,50 @@ CREATE INDEX IF NOT EXISTS idx_system_settings_category ON system_settings(categ
INSERT INTO system_settings (key, value, category) INSERT INTO system_settings (key, value, category)
VALUES VALUES
-- SMTP Settings -- SMTP Settings
('smtp_host', NULL, 'email'), ('smtp_host', NULL, 'email', TRUE),
('smtp_port', NULL, 'email'), ('smtp_port', NULL, 'email', TRUE),
('smtp_user', NULL, 'email'), ('smtp_user', NULL, 'email', TRUE),
('smtp_password', NULL, 'email'), ('smtp_password', NULL, 'email', TRUE),
('smtp_from_email', NULL, 'email'), ('smtp_from_email', NULL, 'email', TRUE),
('smtp_from_name', NULL, 'email'), ('smtp_from_name', NULL, 'email', FALSE),
-- Site Settings -- Site Settings
-- ('site_name', NULL, 'site'), ('site_domain', NULL, 'site', TRUE),
('site_domain', NULL, 'site'), ('site_api_domain', NULL, 'site', TRUE),
('site_api_domain', NULL, 'site'), ('site_protocol', NULL, 'site', TRUE),
('site_protocol', NULL, 'site'), ('site_environment', NULL, 'site', TRUE),
('site_environment', NULL, 'site'), ('site_deployment', NULL, 'site', TRUE),
('site_deployment', NULL, 'site'), ('site_redis_host', NULL, 'site', TRUE),
('site_redis_host', NULL, 'site'), ('site_redis_tls', NULL, 'site', TRUE),
('site_redis_tls', NULL, 'site'), ('site_aws_region', NULL, 'site', TRUE),
('site_aws_region', NULL, 'site'), ('site_aws_s3_bucket', NULL, 'site', TRUE),
('site_aws_s3_bucket', NULL, 'site'), ('site_cdn_domain', NULL, 'site', TRUE),
('site_cdn_domain', NULL, 'site'), ('site_aws_queue_url', NULL, 'site', TRUE),
('site_aws_queue_url', NULL, 'site'), ('site_read_host', NULL, 'site', TRUE),
('site_read_host', NULL, 'site'), ('site_db_max_connections', NULL, 'site', TRUE),
('site_db_max_connections', NULL, 'site'), ('site_session_secret', NULL, 'site', TRUE),
('site_session_secret', NULL, 'site'), ('site_redis_port', NULL, 'site', TRUE),
('site_redis_port', NULL, 'site'), ('site_redis_password', NULL, 'site', TRUE),
('site_redis_password', NULL, 'site'),
-- Payment Settings -- Payment Settings
('currency', 'CAD', 'payment'), ('currency', 'CAD', 'payment', FALSE),
('tax_rate', '0', 'payment'), ('tax_rate', '0', 'payment', FALSE),
-- Shipping Settings -- Shipping Settings
('shipping_flat_rate', '10.00', 'shipping'), ('shipping_flat_rate', '10.00', 'shipping', FALSE),
('shipping_free_threshold', '50.00', 'shipping'), ('shipping_free_threshold', '50.00', 'shipping', FALSE),
('shipping_enabled', 'true', 'shipping'), ('shipping_enabled', 'true', 'shipping', FALSE),
('easypost_api_key', NULL, 'shipping'), ('easypost_api_key', NULL, 'shipping', TRUE),
('easypost_enabled', 'false', 'shipping'), ('easypost_enabled', 'false', 'shipping', FALSE),
('shipping_origin_street', '123 Main St', 'shipping'), ('shipping_origin_street', '123 Main St', 'shipping', FALSE),
('shipping_origin_city', 'Vancouver', 'shipping'), ('shipping_origin_city', 'Vancouver', 'shipping', FALSE),
('shipping_origin_state', 'BC', 'shipping'), ('shipping_origin_state', 'BC', 'shipping', FALSE),
('shipping_origin_zip', 'V6K 1V6', 'shipping'), ('shipping_origin_zip', 'V6K 1V6', 'shipping', FALSE),
('shipping_origin_country', 'CA', 'shipping'), ('shipping_origin_country', 'CA', 'shipping', FALSE),
('shipping_default_package_length', '15', 'shipping'), ('shipping_default_package_length', '15', 'shipping', FALSE),
('shipping_default_package_width', '12', 'shipping'), ('shipping_default_package_width', '12', 'shipping', FALSE),
('shipping_default_package_height', '10', 'shipping'), ('shipping_default_package_height', '10', 'shipping', FALSE),
('shipping_default_package_unit', 'cm', 'shipping'), ('shipping_default_package_unit', 'cm', 'shipping', FALSE),
('shipping_default_weight_unit', 'g', 'shipping'), ('shipping_default_package_weight_unit', 'g', 'shipping', FALSE),
('shipping_carriers_allowed', 'USPS,UPS,FedEx,DHL,Canada Post,Purolator', 'shipping') ('shipping_carriers_allowed', 'USPS,UPS,FedEx,DHL,Canada Post,Purolator', 'shipping', FALSE)
ON CONFLICT (key) DO NOTHING; ON CONFLICT (key) DO NOTHING;

View file

@ -0,0 +1,9 @@
-- Create superadmins table that links to the users table
CREATE TABLE superadmins (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
description TEXT
);
-- Create index for faster lookups
CREATE INDEX idx_superadmins_user_id ON superadmins(user_id);

View file

@ -11,8 +11,8 @@ import {
Chip Chip
} from '@mui/material'; } from '@mui/material';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import couponService from '../services/couponService'; import couponService from '@services/couponService';
import { useAuth } from '../hooks/reduxHooks'; import { useAuth } from '@hooks/reduxHooks';
/** /**
* Component for inputting and applying coupon codes to the cart * Component for inputting and applying coupon codes to the cart

View file

@ -28,17 +28,21 @@ export const authSlice = createSlice({
email: action.payload.email, email: action.payload.email,
firstName: action.payload.firstName, firstName: action.payload.firstName,
lastName: action.payload.lastName, lastName: action.payload.lastName,
isAdmin: action.payload.isAdmin isAdmin: action.payload.isAdmin,
isSuperAdmin: action.payload.isSuperAdmin
}; };
state.apiKey = action.payload.apiKey; state.apiKey = action.payload.apiKey;
state.isAdmin = action.payload.isAdmin; state.isAdmin = action.payload.isAdmin;
state.isSuperAdmin = action.payload.isSuperAdmin;
localStorage.setItem('apiKey', action.payload.apiKey); localStorage.setItem('apiKey', action.payload.apiKey);
localStorage.setItem('isAdmin', action.payload.isAdmin); localStorage.setItem('isAdmin', action.payload.isAdmin);
localStorage.setItem('isSuperAdmin', action.payload.isSuperAdmin);
localStorage.setItem('user', JSON.stringify(action.payload.user)); localStorage.setItem('user', JSON.stringify(action.payload.user));
localStorage.setItem('userData', JSON.stringify({ localStorage.setItem('userData', JSON.stringify({
id: action.payload.user, id: action.payload.user,
apiKey: action.payload.apiKey, apiKey: action.payload.apiKey,
isAdmin: action.payload.isAdmin isAdmin: action.payload.isAdmin,
isSuperAdmin: action.payload.isSuperAdmin
})); }));
}, },
loginFailed: (state, action) => { loginFailed: (state, action) => {
@ -51,8 +55,10 @@ export const authSlice = createSlice({
state.userData = null; state.userData = null;
state.apiKey = null; state.apiKey = null;
state.isAdmin = false; state.isAdmin = false;
state.isSuperAdmin = false;
localStorage.removeItem('apiKey'); localStorage.removeItem('apiKey');
localStorage.removeItem('isAdmin'); localStorage.removeItem('isAdmin');
localStorage.removeItem('isSuperAdmin');
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.removeItem('userData'); localStorage.removeItem('userData');
}, },
@ -67,6 +73,7 @@ export const { loginStart, loginSuccess, loginFailed, logout, clearError } = aut
// Selectors // Selectors
export const selectIsAuthenticated = (state) => state.auth.isAuthenticated; export const selectIsAuthenticated = (state) => state.auth.isAuthenticated;
export const selectIsAdmin = (state) => state.auth.isAdmin; export const selectIsAdmin = (state) => state.auth.isAdmin;
export const selectIsSuperAdmin = (state) => state.auth.isSuperAdmin;
export const selectCurrentUser = (state) => state.auth.user; export const selectCurrentUser = (state) => state.auth.user;
export const selectApiKey = (state) => state.auth.apiKey; export const selectApiKey = (state) => state.auth.apiKey;
export const selectAuthLoading = (state) => state.auth.loading; export const selectAuthLoading = (state) => state.auth.loading;

View file

@ -144,7 +144,7 @@ export const useVerifyCode = () => {
return useMutation({ return useMutation({
mutationFn: (verifyData) => authService.verifyCode(verifyData), mutationFn: (verifyData) => authService.verifyCode(verifyData),
onSuccess: (data) => { onSuccess: (data) => {
login(data.userId, data.apiKey, data.isAdmin, data.email, data?.firstName, data?.lastName); login(data.userId, data.apiKey, data.isAdmin, data.isSuperAdmin, data.email, data?.firstName, data?.lastName);
notification.showNotification('Login successful', 'success'); notification.showNotification('Login successful', 'success');
Clarity.identify(data.userId, data.apiKey); Clarity.identify(data.userId, data.apiKey);
}, },

View file

@ -57,6 +57,7 @@ export const useDarkMode = () => {
export const useAuth = () => { export const useAuth = () => {
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
const isAdmin = useAppSelector((state) => state.auth.isAdmin); const isAdmin = useAppSelector((state) => state.auth.isAdmin);
const isSuperAdmin = useAppSelector((state) => state.auth.isSuperAdmin);
const user = useAppSelector((state) => state.auth.user); const user = useAppSelector((state) => state.auth.user);
const userData = useAppSelector((state) => state.auth.userData); const userData = useAppSelector((state) => state.auth.userData);
const apiKey = useAppSelector((state) => state.auth.apiKey); const apiKey = useAppSelector((state) => state.auth.apiKey);
@ -67,15 +68,16 @@ export const useAuth = () => {
return { return {
isAuthenticated, isAuthenticated,
isAdmin, isAdmin,
isSuperAdmin,
user, user,
userData, userData,
apiKey, apiKey,
loading, loading,
error, error,
login: (user, apiKey, isAdmin, email, firstName = "", lastName = "") => login: (user, apiKey, isAdmin, isSuperAdmin, email, firstName = "", lastName = "") =>
dispatch({ dispatch({
type: 'auth/loginSuccess', type: 'auth/loginSuccess',
payload: { user, apiKey, isAdmin, email, firstName, lastName}}), payload: { user, apiKey, isAdmin, isSuperAdmin, email, firstName, lastName}}),
logout: () => dispatch({ type: 'auth/logout' }), logout: () => dispatch({ type: 'auth/logout' }),
clearError: () => dispatch({ type: 'auth/clearError' }), clearError: () => dispatch({ type: 'auth/clearError' }),
}; };

View file

@ -36,7 +36,9 @@ import {
Add as AddIcon, Add as AddIcon,
Delete as DeleteIcon Delete as DeleteIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAdminSettings, useUpdateSettings, useUpdateSetting, useDeleteSetting } from '../../hooks/settingsAdminHooks'; import { useAdminSettings, useUpdateSettings, useUpdateSetting, useDeleteSetting } from '@hooks/settingsAdminHooks';
import { useAuth } from '@hooks/reduxHooks';
function TabPanel({ children, value, index, ...other }) { function TabPanel({ children, value, index, ...other }) {
return ( return (
@ -69,9 +71,9 @@ const AdminSettingsPage = () => {
const [settingToDelete, setSettingToDelete] = useState(null); const [settingToDelete, setSettingToDelete] = useState(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { isSuperAdmin } = useAuth();
// Fetch settings // Fetch settings
const { data: settingsData, isLoading, error, refetch } = useAdminSettings(); const { data: settingsData, isLoading, error, refetch } = useAdminSettings();
// Update settings mutation // Update settings mutation
const updateSettings = useUpdateSettings(); const updateSettings = useUpdateSettings();
const updateSetting = useUpdateSetting(); const updateSetting = useUpdateSetting();
@ -372,11 +374,11 @@ const AdminSettingsPage = () => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
{setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('key') ? ( {setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('key') ? (
<TextField <TextField
fullWidth fullWidth
name={setting.key} name={setting.key}
disabled={setting.super_req ? !isSuperAdmin : false}
label="Value" label="Value"
type={showPasswords[setting.key] ? 'text' : 'password'} type={showPasswords[setting.key] ? 'text' : 'password'}
value={formData[setting.key] || ''} value={formData[setting.key] || ''}
@ -399,6 +401,7 @@ const AdminSettingsPage = () => {
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
disabled={setting.super_req ? !isSuperAdmin : false}
checked={formData[setting.key] === 'true'} checked={formData[setting.key] === 'true'}
onChange={handleChange} onChange={handleChange}
name={setting.key} name={setting.key}
@ -412,6 +415,7 @@ const AdminSettingsPage = () => {
fullWidth fullWidth
name={setting.key} name={setting.key}
label="Value" label="Value"
disabled={setting.super_req ? !isSuperAdmin : false}
value={formData[setting.key] || ''} value={formData[setting.key] || ''}
onChange={handleChange} onChange={handleChange}
variant="outlined" variant="outlined"

View file

@ -59,10 +59,10 @@ export const applyConsentSettings = (level) => {
if (allowAnalytics) { if (allowAnalytics) {
// Enable Clarity tracking // Enable Clarity tracking
Clarity.consent(); Clarity?.consent();
} else { } else {
// Disable Clarity tracking // Disable Clarity tracking
Clarity.consent({ clarity: false }); Clarity?.consent({ clarity: false });
} }
console.log(`Applied consent level: ${level}, analytics ${allowAnalytics ? 'enabled' : 'disabled'}`); console.log(`Applied consent level: ${level}, analytics ${allowAnalytics ? 'enabled' : 'disabled'}`);