added super admin restrictions for site settings
This commit is contained in:
parent
c35adf0498
commit
b02bbb2086
11 changed files with 130 additions and 57 deletions
|
|
@ -15,8 +15,24 @@ module.exports = (pool, query) => {
|
|||
|
||||
try {
|
||||
// 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(
|
||||
'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]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ module.exports = (pool, query) => {
|
|||
'UPDATE users SET is_admin = TRUE WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
await query(
|
||||
'INSERT INTO superadmins (user_id) values ($1) RETURNING id, created_at',
|
||||
[result.rows[0].id]
|
||||
);
|
||||
}
|
||||
|
||||
await emailService.sendWelcomeEmail({
|
||||
|
|
@ -205,15 +209,33 @@ module.exports = (pool, query) => {
|
|||
);
|
||||
|
||||
// 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(
|
||||
'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]
|
||||
);
|
||||
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
userId: userId,
|
||||
isAdmin: userInfo.rows[0].is_admin,
|
||||
isSuperAdmin: userInfo.rows[0].is_super_admin,
|
||||
firstName: userInfo.rows[0].first_name,
|
||||
lastName: userInfo.rows[0].last_name,
|
||||
email: userInfo.rows[0].email,
|
||||
|
|
|
|||
|
|
@ -113,7 +113,14 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
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);
|
||||
|
||||
// Update config in memory
|
||||
|
|
@ -137,7 +144,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
router.post('/batch', async (req, res, next) => {
|
||||
try {
|
||||
const { settings } = req.body;
|
||||
|
||||
console.log(req.user)
|
||||
|
||||
if (!req.user.is_admin) {
|
||||
return res.status(403).json({
|
||||
|
|
@ -152,7 +159,7 @@ module.exports = (pool, query, authMiddleware) => {
|
|||
message: 'Settings array is required'
|
||||
});
|
||||
}
|
||||
|
||||
const categorySettings = await SystemSettings.getSettingsByCategory(pool, query, settings[0]?.category)
|
||||
// Validate all settings have required fields
|
||||
for (const setting of settings) {
|
||||
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'
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS system_settings (
|
|||
key VARCHAR(255) NOT NULL UNIQUE,
|
||||
value TEXT,
|
||||
category VARCHAR(100) NOT NULL,
|
||||
super_req BOOLEAN DEFAULT FALSE,
|
||||
created_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)
|
||||
VALUES
|
||||
-- SMTP Settings
|
||||
('smtp_host', NULL, 'email'),
|
||||
('smtp_port', NULL, 'email'),
|
||||
('smtp_user', NULL, 'email'),
|
||||
('smtp_password', NULL, 'email'),
|
||||
('smtp_from_email', NULL, 'email'),
|
||||
('smtp_from_name', NULL, 'email'),
|
||||
('smtp_host', NULL, 'email', TRUE),
|
||||
('smtp_port', NULL, 'email', TRUE),
|
||||
('smtp_user', NULL, 'email', TRUE),
|
||||
('smtp_password', NULL, 'email', TRUE),
|
||||
('smtp_from_email', NULL, 'email', TRUE),
|
||||
('smtp_from_name', NULL, 'email', FALSE),
|
||||
|
||||
-- Site Settings
|
||||
-- ('site_name', NULL, 'site'),
|
||||
('site_domain', NULL, 'site'),
|
||||
('site_api_domain', NULL, 'site'),
|
||||
('site_protocol', NULL, 'site'),
|
||||
('site_environment', NULL, 'site'),
|
||||
('site_deployment', NULL, 'site'),
|
||||
('site_redis_host', NULL, 'site'),
|
||||
('site_redis_tls', NULL, 'site'),
|
||||
('site_aws_region', NULL, 'site'),
|
||||
('site_aws_s3_bucket', NULL, 'site'),
|
||||
('site_cdn_domain', NULL, 'site'),
|
||||
('site_aws_queue_url', NULL, 'site'),
|
||||
('site_read_host', NULL, 'site'),
|
||||
('site_db_max_connections', NULL, 'site'),
|
||||
('site_session_secret', NULL, 'site'),
|
||||
('site_redis_port', NULL, 'site'),
|
||||
('site_redis_password', NULL, 'site'),
|
||||
('site_domain', NULL, 'site', TRUE),
|
||||
('site_api_domain', NULL, 'site', TRUE),
|
||||
('site_protocol', NULL, 'site', TRUE),
|
||||
('site_environment', NULL, 'site', TRUE),
|
||||
('site_deployment', NULL, 'site', TRUE),
|
||||
('site_redis_host', NULL, 'site', TRUE),
|
||||
('site_redis_tls', NULL, 'site', TRUE),
|
||||
('site_aws_region', NULL, 'site', TRUE),
|
||||
('site_aws_s3_bucket', NULL, 'site', TRUE),
|
||||
('site_cdn_domain', NULL, 'site', TRUE),
|
||||
('site_aws_queue_url', NULL, 'site', TRUE),
|
||||
('site_read_host', NULL, 'site', TRUE),
|
||||
('site_db_max_connections', NULL, 'site', TRUE),
|
||||
('site_session_secret', NULL, 'site', TRUE),
|
||||
('site_redis_port', NULL, 'site', TRUE),
|
||||
('site_redis_password', NULL, 'site', TRUE),
|
||||
|
||||
-- Payment Settings
|
||||
('currency', 'CAD', 'payment'),
|
||||
('tax_rate', '0', 'payment'),
|
||||
('currency', 'CAD', 'payment', FALSE),
|
||||
('tax_rate', '0', 'payment', FALSE),
|
||||
|
||||
-- Shipping Settings
|
||||
('shipping_flat_rate', '10.00', 'shipping'),
|
||||
('shipping_free_threshold', '50.00', 'shipping'),
|
||||
('shipping_enabled', 'true', 'shipping'),
|
||||
('easypost_api_key', NULL, 'shipping'),
|
||||
('easypost_enabled', 'false', 'shipping'),
|
||||
('shipping_origin_street', '123 Main St', 'shipping'),
|
||||
('shipping_origin_city', 'Vancouver', 'shipping'),
|
||||
('shipping_origin_state', 'BC', 'shipping'),
|
||||
('shipping_origin_zip', 'V6K 1V6', 'shipping'),
|
||||
('shipping_origin_country', 'CA', 'shipping'),
|
||||
('shipping_default_package_length', '15', 'shipping'),
|
||||
('shipping_default_package_width', '12', 'shipping'),
|
||||
('shipping_default_package_height', '10', 'shipping'),
|
||||
('shipping_default_package_unit', 'cm', 'shipping'),
|
||||
('shipping_default_weight_unit', 'g', 'shipping'),
|
||||
('shipping_carriers_allowed', 'USPS,UPS,FedEx,DHL,Canada Post,Purolator', 'shipping')
|
||||
('shipping_flat_rate', '10.00', 'shipping', FALSE),
|
||||
('shipping_free_threshold', '50.00', 'shipping', FALSE),
|
||||
('shipping_enabled', 'true', 'shipping', FALSE),
|
||||
('easypost_api_key', NULL, 'shipping', TRUE),
|
||||
('easypost_enabled', 'false', 'shipping', FALSE),
|
||||
('shipping_origin_street', '123 Main St', 'shipping', FALSE),
|
||||
('shipping_origin_city', 'Vancouver', 'shipping', FALSE),
|
||||
('shipping_origin_state', 'BC', 'shipping', FALSE),
|
||||
('shipping_origin_zip', 'V6K 1V6', 'shipping', FALSE),
|
||||
('shipping_origin_country', 'CA', 'shipping', FALSE),
|
||||
('shipping_default_package_length', '15', 'shipping', FALSE),
|
||||
('shipping_default_package_width', '12', 'shipping', FALSE),
|
||||
('shipping_default_package_height', '10', 'shipping', FALSE),
|
||||
('shipping_default_package_unit', 'cm', 'shipping', FALSE),
|
||||
('shipping_default_package_weight_unit', 'g', 'shipping', FALSE),
|
||||
('shipping_carriers_allowed', 'USPS,UPS,FedEx,DHL,Canada Post,Purolator', 'shipping', FALSE)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
9
db/init/23-suparadmin.sql
Normal file
9
db/init/23-suparadmin.sql
Normal 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);
|
||||
|
|
@ -11,8 +11,8 @@ import {
|
|||
Chip
|
||||
} from '@mui/material';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import couponService from '../services/couponService';
|
||||
import { useAuth } from '../hooks/reduxHooks';
|
||||
import couponService from '@services/couponService';
|
||||
import { useAuth } from '@hooks/reduxHooks';
|
||||
|
||||
/**
|
||||
* Component for inputting and applying coupon codes to the cart
|
||||
|
|
|
|||
|
|
@ -28,17 +28,21 @@ export const authSlice = createSlice({
|
|||
email: action.payload.email,
|
||||
firstName: action.payload.firstName,
|
||||
lastName: action.payload.lastName,
|
||||
isAdmin: action.payload.isAdmin
|
||||
isAdmin: action.payload.isAdmin,
|
||||
isSuperAdmin: action.payload.isSuperAdmin
|
||||
};
|
||||
state.apiKey = action.payload.apiKey;
|
||||
state.isAdmin = action.payload.isAdmin;
|
||||
state.isSuperAdmin = action.payload.isSuperAdmin;
|
||||
localStorage.setItem('apiKey', action.payload.apiKey);
|
||||
localStorage.setItem('isAdmin', action.payload.isAdmin);
|
||||
localStorage.setItem('isSuperAdmin', action.payload.isSuperAdmin);
|
||||
localStorage.setItem('user', JSON.stringify(action.payload.user));
|
||||
localStorage.setItem('userData', JSON.stringify({
|
||||
id: action.payload.user,
|
||||
apiKey: action.payload.apiKey,
|
||||
isAdmin: action.payload.isAdmin
|
||||
isAdmin: action.payload.isAdmin,
|
||||
isSuperAdmin: action.payload.isSuperAdmin
|
||||
}));
|
||||
},
|
||||
loginFailed: (state, action) => {
|
||||
|
|
@ -51,8 +55,10 @@ export const authSlice = createSlice({
|
|||
state.userData = null;
|
||||
state.apiKey = null;
|
||||
state.isAdmin = false;
|
||||
state.isSuperAdmin = false;
|
||||
localStorage.removeItem('apiKey');
|
||||
localStorage.removeItem('isAdmin');
|
||||
localStorage.removeItem('isSuperAdmin');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('userData');
|
||||
},
|
||||
|
|
@ -67,6 +73,7 @@ export const { loginStart, loginSuccess, loginFailed, logout, clearError } = aut
|
|||
// Selectors
|
||||
export const selectIsAuthenticated = (state) => state.auth.isAuthenticated;
|
||||
export const selectIsAdmin = (state) => state.auth.isAdmin;
|
||||
export const selectIsSuperAdmin = (state) => state.auth.isSuperAdmin;
|
||||
export const selectCurrentUser = (state) => state.auth.user;
|
||||
export const selectApiKey = (state) => state.auth.apiKey;
|
||||
export const selectAuthLoading = (state) => state.auth.loading;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export const useVerifyCode = () => {
|
|||
return useMutation({
|
||||
mutationFn: (verifyData) => authService.verifyCode(verifyData),
|
||||
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');
|
||||
Clarity.identify(data.userId, data.apiKey);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export const useDarkMode = () => {
|
|||
export const useAuth = () => {
|
||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
||||
const isAdmin = useAppSelector((state) => state.auth.isAdmin);
|
||||
const isSuperAdmin = useAppSelector((state) => state.auth.isSuperAdmin);
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const userData = useAppSelector((state) => state.auth.userData);
|
||||
const apiKey = useAppSelector((state) => state.auth.apiKey);
|
||||
|
|
@ -67,15 +68,16 @@ export const useAuth = () => {
|
|||
return {
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
isSuperAdmin,
|
||||
user,
|
||||
userData,
|
||||
apiKey,
|
||||
loading,
|
||||
error,
|
||||
login: (user, apiKey, isAdmin, email, firstName = "", lastName = "") =>
|
||||
login: (user, apiKey, isAdmin, isSuperAdmin, email, firstName = "", lastName = "") =>
|
||||
dispatch({
|
||||
type: 'auth/loginSuccess',
|
||||
payload: { user, apiKey, isAdmin, email, firstName, lastName}}),
|
||||
payload: { user, apiKey, isAdmin, isSuperAdmin, email, firstName, lastName}}),
|
||||
logout: () => dispatch({ type: 'auth/logout' }),
|
||||
clearError: () => dispatch({ type: 'auth/clearError' }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ import {
|
|||
Add as AddIcon,
|
||||
Delete as DeleteIcon
|
||||
} 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 }) {
|
||||
return (
|
||||
|
|
@ -69,9 +71,9 @@ const AdminSettingsPage = () => {
|
|||
const [settingToDelete, setSettingToDelete] = useState(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const { isSuperAdmin } = useAuth();
|
||||
// Fetch settings
|
||||
const { data: settingsData, isLoading, error, refetch } = useAdminSettings();
|
||||
|
||||
// Update settings mutation
|
||||
const updateSettings = useUpdateSettings();
|
||||
const updateSetting = useUpdateSetting();
|
||||
|
|
@ -372,11 +374,11 @@ const AdminSettingsPage = () => {
|
|||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('key') ? (
|
||||
<TextField
|
||||
fullWidth
|
||||
name={setting.key}
|
||||
disabled={setting.super_req ? !isSuperAdmin : false}
|
||||
label="Value"
|
||||
type={showPasswords[setting.key] ? 'text' : 'password'}
|
||||
value={formData[setting.key] || ''}
|
||||
|
|
@ -399,6 +401,7 @@ const AdminSettingsPage = () => {
|
|||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={setting.super_req ? !isSuperAdmin : false}
|
||||
checked={formData[setting.key] === 'true'}
|
||||
onChange={handleChange}
|
||||
name={setting.key}
|
||||
|
|
@ -412,6 +415,7 @@ const AdminSettingsPage = () => {
|
|||
fullWidth
|
||||
name={setting.key}
|
||||
label="Value"
|
||||
disabled={setting.super_req ? !isSuperAdmin : false}
|
||||
value={formData[setting.key] || ''}
|
||||
onChange={handleChange}
|
||||
variant="outlined"
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ export const applyConsentSettings = (level) => {
|
|||
|
||||
if (allowAnalytics) {
|
||||
// Enable Clarity tracking
|
||||
Clarity.consent();
|
||||
Clarity?.consent();
|
||||
} else {
|
||||
// Disable Clarity tracking
|
||||
Clarity.consent({ clarity: false });
|
||||
Clarity?.consent({ clarity: false });
|
||||
}
|
||||
|
||||
console.log(`Applied consent level: ${level}, analytics ${allowAnalytics ? 'enabled' : 'disabled'}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue