Fix jwt-token
This commit is contained in:
@@ -216,17 +216,6 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Demo credentials for local/ldap auth */}
|
|
||||||
{(tenantConfig?.auth_provider === 'local' || tenantConfig?.auth_provider === 'ldap') && (
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Demo credentials: <br />
|
|
||||||
Username: <code className="bg-gray-100 px-1 rounded">admin</code> <br />
|
|
||||||
Password: <code className="bg-gray-100 px-1 rounded">admin123</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error message if auth provider not configured */}
|
{/* Error message if auth provider not configured */}
|
||||||
{!tenantConfig?.auth_provider && (
|
{!tenantConfig?.auth_provider && (
|
||||||
<div className="mt-8 p-4 bg-red-50 border border-red-200 rounded-md">
|
<div className="mt-8 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
|||||||
@@ -69,11 +69,16 @@ const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => {
|
|||||||
primary_color: '#3B82F6',
|
primary_color: '#3B82F6',
|
||||||
secondary_color: '#1F2937',
|
secondary_color: '#1F2937',
|
||||||
company_name: ''
|
company_name: ''
|
||||||
}
|
},
|
||||||
|
// IP Restriction settings
|
||||||
|
ip_restriction_enabled: false,
|
||||||
|
ip_whitelist: [],
|
||||||
|
ip_restriction_message: 'Access denied. Your IP address is not authorized to access this tenant.'
|
||||||
})
|
})
|
||||||
|
|
||||||
const [showSecrets, setShowSecrets] = useState(false)
|
const [showSecrets, setShowSecrets] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [newIP, setNewIP] = useState('')
|
||||||
|
|
||||||
// Load tenant data when editing
|
// Load tenant data when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -85,7 +90,11 @@ const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => {
|
|||||||
user_mapping: { ...formData.user_mapping, ...tenant.user_mapping },
|
user_mapping: { ...formData.user_mapping, ...tenant.user_mapping },
|
||||||
role_mapping: { ...formData.role_mapping, ...tenant.role_mapping },
|
role_mapping: { ...formData.role_mapping, ...tenant.role_mapping },
|
||||||
features: { ...formData.features, ...tenant.features },
|
features: { ...formData.features, ...tenant.features },
|
||||||
branding: { ...formData.branding, ...tenant.branding }
|
branding: { ...formData.branding, ...tenant.branding },
|
||||||
|
// IP restriction fields with fallbacks
|
||||||
|
ip_restriction_enabled: tenant.ip_restriction_enabled || false,
|
||||||
|
ip_whitelist: tenant.ip_whitelist || [],
|
||||||
|
ip_restriction_message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this tenant.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [tenant])
|
}, [tenant])
|
||||||
@@ -106,6 +115,43 @@ const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP Management functions
|
||||||
|
const addIPToWhitelist = () => {
|
||||||
|
if (!newIP.trim()) {
|
||||||
|
toast.error('Please enter an IP address or CIDR block')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation for IP format
|
||||||
|
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$|^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\*$/
|
||||||
|
|
||||||
|
if (!ipPattern.test(newIP.trim())) {
|
||||||
|
toast.error('Please enter a valid IP address, CIDR block (e.g., 192.168.1.0/24), or wildcard (e.g., 192.168.1.*)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = newIP.trim()
|
||||||
|
if (formData.ip_whitelist.includes(ip)) {
|
||||||
|
toast.error('This IP is already in the whitelist')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ip_whitelist: [...prev.ip_whitelist, ip]
|
||||||
|
}))
|
||||||
|
setNewIP('')
|
||||||
|
toast.success('IP added to whitelist')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeIPFromWhitelist = (ipToRemove) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ip_whitelist: prev.ip_whitelist.filter(ip => ip !== ipToRemove)
|
||||||
|
}))
|
||||||
|
toast.success('IP removed from whitelist')
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -604,6 +650,120 @@ const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* IP Restriction Configuration */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-md font-medium text-gray-900 border-b pb-2">IP Access Control</h4>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="ip_restriction_enabled"
|
||||||
|
checked={formData.ip_restriction_enabled}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ip_restriction_enabled: e.target.checked
|
||||||
|
}))}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="ip_restriction_enabled" className="text-sm font-medium text-gray-700">
|
||||||
|
Enable IP Restrictions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.ip_restriction_enabled && (
|
||||||
|
<div className="space-y-4 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Only IPs in the whitelist below will be able to access this tenant. You can add individual IP addresses, CIDR blocks, or wildcards.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Add IP Input */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter IP address (e.g., 192.168.1.100, 10.0.0.0/24, or 192.168.1.*)"
|
||||||
|
value={newIP}
|
||||||
|
onChange={(e) => setNewIP(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
addIPToWhitelist()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addIPToWhitelist}
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Add IP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IP Whitelist */}
|
||||||
|
{formData.ip_whitelist.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Allowed IP Addresses ({formData.ip_whitelist.length})
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2 max-h-32 overflow-y-auto">
|
||||||
|
{formData.ip_whitelist.map((ip, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between bg-white px-3 py-2 rounded border">
|
||||||
|
<span className="text-sm font-mono">{ip}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeIPFromWhitelist(ip)}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom restriction message */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Access Denied Message
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.ip_restriction_message}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
ip_restriction_message: e.target.value
|
||||||
|
}))}
|
||||||
|
rows={3}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Message shown when access is denied due to IP restrictions"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
This message will be shown to users whose IP is not in the whitelist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-yellow-800">
|
||||||
|
⚠️ Important Security Notes
|
||||||
|
</h3>
|
||||||
|
<div className="mt-1 text-sm text-yellow-700">
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li>Make sure to include your current IP to avoid being locked out</li>
|
||||||
|
<li>IP restrictions apply to all tenant access including login and API calls</li>
|
||||||
|
<li>Use CIDR notation for IP ranges (e.g., 192.168.1.0/24)</li>
|
||||||
|
<li>Use wildcards for partial matching (e.g., 192.168.1.*)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Form Actions */}
|
{/* Form Actions */}
|
||||||
<div className="flex justify-end space-x-3 pt-6 border-t">
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const { initializeHealthService } = require('./routes/deviceHealth');
|
|||||||
const seedDatabase = require('./seedDatabase');
|
const seedDatabase = require('./seedDatabase');
|
||||||
const errorHandler = require('./middleware/errorHandler');
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
const { apiDebugMiddleware } = require('./utils/apiDebugLogger');
|
const { apiDebugMiddleware } = require('./utils/apiDebugLogger');
|
||||||
|
const IPRestrictionMiddleware = require('./middleware/ip-restriction');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -79,6 +80,10 @@ if (process.env.API_DEBUG === 'true') {
|
|||||||
|
|
||||||
app.use('/api/', limiter);
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// IP Restriction middleware (before routes)
|
||||||
|
const ipRestriction = new IPRestrictionMiddleware();
|
||||||
|
app.use((req, res, next) => ipRestriction.checkIPRestriction(req, res, next));
|
||||||
|
|
||||||
// Make io available to routes
|
// Make io available to routes
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.io = io;
|
req.io = io;
|
||||||
|
|||||||
206
server/middleware/ip-restriction.js
Normal file
206
server/middleware/ip-restriction.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* IP Restriction Middleware
|
||||||
|
* Checks if the client IP is allowed access based on tenant configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Tenant } = require('../models');
|
||||||
|
const MultiTenantAuth = require('./multi-tenant-auth');
|
||||||
|
|
||||||
|
class IPRestrictionMiddleware {
|
||||||
|
constructor() {
|
||||||
|
this.multiAuth = new MultiTenantAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP address matches any pattern in the whitelist
|
||||||
|
* @param {string} clientIP - The client IP address
|
||||||
|
* @param {Array} whitelist - Array of IP addresses and CIDR blocks
|
||||||
|
* @returns {boolean} - True if IP is allowed
|
||||||
|
*/
|
||||||
|
isIPAllowed(clientIP, whitelist) {
|
||||||
|
if (!whitelist || !Array.isArray(whitelist) || whitelist.length === 0) {
|
||||||
|
return true; // No restrictions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
|
const normalizedIP = clientIP.replace(/^::ffff:/, '');
|
||||||
|
|
||||||
|
for (const allowedIP of whitelist) {
|
||||||
|
if (this.matchesPattern(normalizedIP, allowedIP.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP matches a pattern (IP address or CIDR block)
|
||||||
|
* @param {string} ip - The IP to check
|
||||||
|
* @param {string} pattern - IP address or CIDR block
|
||||||
|
* @returns {boolean} - True if IP matches pattern
|
||||||
|
*/
|
||||||
|
matchesPattern(ip, pattern) {
|
||||||
|
// Exact IP match
|
||||||
|
if (ip === pattern) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIDR block matching
|
||||||
|
if (pattern.includes('/')) {
|
||||||
|
return this.isIPInCIDR(ip, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard matching (e.g., 192.168.1.*)
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + '$');
|
||||||
|
return regex.test(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP is within a CIDR block
|
||||||
|
* @param {string} ip - The IP to check
|
||||||
|
* @param {string} cidr - CIDR block (e.g., "192.168.1.0/24")
|
||||||
|
* @returns {boolean} - True if IP is in CIDR block
|
||||||
|
*/
|
||||||
|
isIPInCIDR(ip, cidr) {
|
||||||
|
try {
|
||||||
|
const [subnet, prefixLength] = cidr.split('/');
|
||||||
|
const prefix = parseInt(prefixLength, 10);
|
||||||
|
|
||||||
|
if (prefix < 0 || prefix > 32) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipNum = this.ipToNumber(ip);
|
||||||
|
const subnetNum = this.ipToNumber(subnet);
|
||||||
|
const mask = (-1 << (32 - prefix)) >>> 0;
|
||||||
|
|
||||||
|
return (ipNum & mask) === (subnetNum & mask);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking CIDR:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert IP address to number
|
||||||
|
* @param {string} ip - IP address
|
||||||
|
* @returns {number} - IP as number
|
||||||
|
*/
|
||||||
|
ipToNumber(ip) {
|
||||||
|
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP from request, considering proxy headers
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @returns {string} - Client IP address
|
||||||
|
*/
|
||||||
|
getClientIP(req) {
|
||||||
|
// Check various headers for real IP (in order of preference)
|
||||||
|
const possibleHeaders = [
|
||||||
|
'x-forwarded-for',
|
||||||
|
'x-real-ip',
|
||||||
|
'x-client-ip',
|
||||||
|
'cf-connecting-ip', // Cloudflare
|
||||||
|
'x-cluster-client-ip',
|
||||||
|
'x-forwarded',
|
||||||
|
'forwarded-for',
|
||||||
|
'forwarded'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const header of possibleHeaders) {
|
||||||
|
const ip = req.headers[header];
|
||||||
|
if (ip) {
|
||||||
|
// x-forwarded-for can contain multiple IPs, get the first one
|
||||||
|
const firstIP = ip.split(',')[0].trim();
|
||||||
|
if (this.isValidIP(firstIP)) {
|
||||||
|
return firstIP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to connection IP
|
||||||
|
return req.connection.remoteAddress || req.socket.remoteAddress || req.ip || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic IP validation
|
||||||
|
* @param {string} ip - IP address to validate
|
||||||
|
* @returns {boolean} - True if valid IP
|
||||||
|
*/
|
||||||
|
isValidIP(ip) {
|
||||||
|
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware to check IP restrictions
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
* @param {Function} next - Next middleware function
|
||||||
|
*/
|
||||||
|
async checkIPRestriction(req, res, next) {
|
||||||
|
try {
|
||||||
|
// Skip IP checking for health checks and internal requests
|
||||||
|
if (req.path === '/health' || req.path === '/api/health') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine tenant
|
||||||
|
const tenantId = await this.multiAuth.determineTenant(req);
|
||||||
|
if (!tenantId) {
|
||||||
|
// No tenant found, continue without IP checking
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tenant configuration
|
||||||
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if IP restrictions are enabled
|
||||||
|
if (!tenant.ip_restriction_enabled) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get client IP
|
||||||
|
const clientIP = this.getClientIP(req);
|
||||||
|
|
||||||
|
// Check if IP is allowed
|
||||||
|
const isAllowed = this.isIPAllowed(clientIP, tenant.ip_whitelist);
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
console.log(`🚫 IP Access Denied: ${clientIP} attempted to access tenant "${tenantId}"`);
|
||||||
|
|
||||||
|
// Log the access attempt for security auditing
|
||||||
|
console.log(`[SECURITY AUDIT] ${new Date().toISOString()} - IP ${clientIP} denied access to tenant ${tenantId} - User-Agent: ${req.headers['user-agent']}`);
|
||||||
|
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this service.',
|
||||||
|
code: 'IP_RESTRICTED',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP is allowed, continue
|
||||||
|
console.log(`✅ IP Access Allowed: ${clientIP} accessing tenant "${tenantId}"`);
|
||||||
|
next();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in IP restriction middleware:', error);
|
||||||
|
// In case of error, allow access but log the issue
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = IPRestrictionMiddleware;
|
||||||
@@ -82,6 +82,25 @@ module.exports = (sequelize) => {
|
|||||||
comment: 'Tenant feature limits and enabled features'
|
comment: 'Tenant feature limits and enabled features'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Security Configuration
|
||||||
|
ip_whitelist: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
comment: 'Array of allowed IP addresses/CIDR blocks for this tenant'
|
||||||
|
},
|
||||||
|
ip_restriction_enabled: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: 'Whether IP restrictions are enabled for this tenant'
|
||||||
|
},
|
||||||
|
ip_restriction_message: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 'Access denied. Your IP address is not authorized to access this tenant.',
|
||||||
|
comment: 'Custom message shown when IP access is denied'
|
||||||
|
},
|
||||||
|
|
||||||
// Contact Information
|
// Contact Information
|
||||||
admin_email: {
|
admin_email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
|
|||||||
Reference in New Issue
Block a user