Supabase Admin Dashboard Security: Best Practices & Implementation
Security is paramount in admin dashboards. This comprehensive guide covers essential security practices for building secure Supabase-powered admin dashboards that protect sensitive data and user privacy.
1. Authentication Security Fundamentals
Multi-Factor Authentication (MFA)
Implement MFA for all admin users to add an extra layer of security:
// Enable MFA for admin users
const enableMFA = async (userId: string) => {
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp'
});
if (error) throw error;
return data;
};
// Verify MFA during login
const verifyMFA = async (factorId: string, code: string) => {
const { data, error } = await supabase.auth.mfa.verify({
factorId,
code
});
if (error) throw error;
return data;
};
// Check if user has MFA enabled
const checkMFAStatus = async (userId: string) => {
const { data, error } = await supabase.auth.mfa.listFactors();
if (error) throw error;
return data.totp.length > 0;
};Session Management
Implement secure session management with proper timeouts and refresh tokens:
// Configure session settings
const sessionConfig = {
// Short-lived access tokens (15 minutes)
accessTokenExpiry: 15 * 60,
// Longer refresh tokens (7 days)
refreshTokenExpiry: 7 * 24 * 60 * 60,
// Auto-refresh before expiry
autoRefreshThreshold: 5 * 60
};
// Session monitoring
const monitorSession = () => {
const { data: { session } } = supabase.auth.getSession();
if (session) {
const expiresAt = new Date(session.expires_at! * 1000);
const now = new Date();
const timeUntilExpiry = expiresAt.getTime() - now.getTime();
// Auto-refresh if less than 5 minutes remaining
if (timeUntilExpiry < sessionConfig.autoRefreshThreshold * 1000) {
supabase.auth.refreshSession();
}
}
};
// Set up session monitoring
setInterval(monitorSession, 60000); // Check every minute2. Row Level Security (RLS) Implementation
Comprehensive RLS Policies
Create granular RLS policies for different user roles and data access levels:
-- Enable RLS on all sensitive tables
ALTER TABLE public.sensitive_data ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.admin_logs ENABLE ROW LEVEL SECURITY;
-- Admin-only access policies
CREATE POLICY "Admins can manage all data" ON public.sensitive_data
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.user_profiles
WHERE id = auth.uid()
AND role = 'admin'
AND status = 'active'
)
);
-- Moderator read-only access
CREATE POLICY "Moderators can read data" ON public.sensitive_data
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.user_profiles
WHERE id = auth.uid()
AND role IN ('admin', 'moderator')
AND status = 'active'
)
);
-- User-specific data access
CREATE POLICY "Users can access own data" ON public.user_profiles
FOR ALL USING (auth.uid() = id);
-- Time-based access control
CREATE POLICY "Business hours only" ON public.admin_logs
FOR ALL USING (
EXTRACT(HOUR FROM NOW()) BETWEEN 8 AND 18
AND EXISTS (
SELECT 1 FROM public.user_profiles
WHERE id = auth.uid()
AND role = 'admin'
)
);3. API Security and Rate Limiting
Implementing Rate Limiting
Protect your admin dashboard from abuse with proper rate limiting:
// Rate limiting implementation
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(
private maxRequests: number = 100,
private windowMs: number = 15 * 60 * 1000 // 15 minutes
) {}
isAllowed(userId: string): boolean {
const now = Date.now();
const userRequests = this.requests.get(userId) || [];
// Remove old requests outside the window
const validRequests = userRequests.filter(
timestamp => now - timestamp < this.windowMs
);
if (validRequests.length >= this.maxRequests) {
return false;
}
// Add current request
validRequests.push(now);
this.requests.set(userId, validRequests);
return true;
}
}
// Apply rate limiting to admin operations
const rateLimiter = new RateLimiter(50, 15 * 60 * 1000); // 50 requests per 15 minutes
const adminOperation = async (userId: string, operation: string) => {
if (!rateLimiter.isAllowed(userId)) {
throw new Error('Rate limit exceeded');
}
// Log the operation
await logAdminAction(userId, operation);
// Perform the operation
return await performOperation(operation);
};Input Validation and Sanitization
Implement comprehensive input validation to prevent injection attacks:
// Input validation utilities
const validateInput = {
email: (email: string): boolean => {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email) && email.length <= 254;
},
password: (password: string): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
if (password.length < 12) {
errors.push('Password must be at least 12 characters long');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/d/.test(password)) {
errors.push('Password must contain at least one number');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Password must contain at least one special character');
}
return { valid: errors.length === 0, errors };
},
sanitizeString: (input: string): string => {
return input
.replace(/[<>]/g, '') // Remove potential HTML tags
.replace(/['"]/g, '') // Remove quotes
.trim()
.substring(0, 1000); // Limit length
}
};
// Use validation in admin operations
const createUser = async (userData: any) => {
// Validate email
if (!validateInput.email(userData.email)) {
throw new Error('Invalid email format');
}
// Validate password
const passwordValidation = validateInput.password(userData.password);
if (!passwordValidation.valid) {
throw new Error(passwordValidation.errors.join(', '));
}
// Sanitize other inputs
const sanitizedData = {
...userData,
full_name: validateInput.sanitizeString(userData.full_name),
email: userData.email.toLowerCase().trim()
};
return await supabase.auth.admin.createUser(sanitizedData);
};4. Data Encryption and Protection
Encrypting Sensitive Data
Implement encryption for sensitive data at rest and in transit:
// Client-side encryption for sensitive data
import CryptoJS from 'crypto-js';
class DataEncryption {
private static readonly SECRET_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY!;
static encrypt(data: string): string {
return CryptoJS.AES.encrypt(data, this.SECRET_KEY).toString();
}
static decrypt(encryptedData: string): string {
const bytes = CryptoJS.AES.decrypt(encryptedData, this.SECRET_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
}
static hashPassword(password: string): string {
return CryptoJS.SHA256(password + this.SECRET_KEY).toString();
}
}
// Encrypt sensitive data before storing
const storeSensitiveData = async (data: any) => {
const encryptedData = {
...data,
ssn: DataEncryption.encrypt(data.ssn),
credit_card: DataEncryption.encrypt(data.credit_card),
personal_notes: DataEncryption.encrypt(data.personal_notes)
};
const { data: result, error } = await supabase
.from('sensitive_data')
.insert(encryptedData);
if (error) throw error;
return result;
};
// Decrypt data when retrieving
const getSensitiveData = async (id: string) => {
const { data, error } = await supabase
.from('sensitive_data')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
// Decrypt sensitive fields
return {
...data,
ssn: DataEncryption.decrypt(data.ssn),
credit_card: DataEncryption.decrypt(data.credit_card),
personal_notes: DataEncryption.decrypt(data.personal_notes)
};
};5. Audit Logging and Monitoring
Comprehensive Audit Trail
Implement detailed audit logging for all admin actions:
// Audit logging system
interface AuditLog {
id: string;
user_id: string;
action: string;
resource: string;
resource_id?: string;
old_values?: any;
new_values?: any;
ip_address: string;
user_agent: string;
timestamp: Date;
success: boolean;
error_message?: string;
}
class AuditLogger {
static async logAction(
userId: string,
action: string,
resource: string,
details: Partial<AuditLog> = {}
) {
const auditLog: Omit<AuditLog, 'id' | 'timestamp'> = {
user_id: userId,
action,
resource,
ip_address: details.ip_address || 'unknown',
user_agent: details.user_agent || 'unknown',
success: details.success ?? true,
error_message: details.error_message,
resource_id: details.resource_id,
old_values: details.old_values,
new_values: details.new_values
};
const { error } = await supabase
.from('audit_logs')
.insert(auditLog);
if (error) {
console.error('Failed to log audit action:', error);
}
}
static async getAuditTrail(
userId?: string,
action?: string,
limit: number = 100
) {
let query = supabase
.from('audit_logs')
.select('*')
.order('timestamp', { ascending: false })
.limit(limit);
if (userId) {
query = query.eq('user_id', userId);
}
if (action) {
query = query.eq('action', action);
}
const { data, error } = await query;
if (error) throw error;
return data;
}
}
// Use audit logging in admin operations
const deleteUser = async (adminId: string, targetUserId: string) => {
try {
// Get current user data for audit
const { data: oldUserData } = await supabase
.from('user_profiles')
.select('*')
.eq('id', targetUserId)
.single();
// Perform deletion
const { error } = await supabase
.from('user_profiles')
.delete()
.eq('id', targetUserId);
if (error) throw error;
// Log successful deletion
await AuditLogger.logAction(adminId, 'DELETE_USER', 'user_profiles', {
resource_id: targetUserId,
old_values: oldUserData,
success: true
});
return { success: true };
} catch (error) {
// Log failed deletion
await AuditLogger.logAction(adminId, 'DELETE_USER', 'user_profiles', {
resource_id: targetUserId,
success: false,
error_message: error.message
});
throw error;
}
};6. Security Headers and CORS Configuration
Implementing Security Headers
Configure proper security headers for your admin dashboard:
// Next.js security headers configuration
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self' https://*.supabase.co wss://*.supabase.co",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; ')
}
]
}
];
}
};
module.exports = nextConfig;7. Compliance and Data Privacy
GDPR Compliance Implementation
Implement GDPR compliance features for data privacy:
// GDPR compliance utilities
class GDPRCompliance {
// Right to be forgotten - Delete user data
static async deleteUserData(userId: string) {
const tables = [
'user_profiles',
'user_preferences',
'audit_logs',
'sensitive_data'
];
for (const table of tables) {
await supabase
.from(table)
.delete()
.eq('user_id', userId);
}
// Anonymize audit logs (keep for legal compliance)
await supabase
.from('audit_logs')
.update({
user_id: null,
ip_address: 'anonymized',
user_agent: 'anonymized'
})
.eq('user_id', userId);
}
// Data portability - Export user data
static async exportUserData(userId: string) {
const userData = await supabase
.from('user_profiles')
.select('*')
.eq('id', userId)
.single();
const preferences = await supabase
.from('user_preferences')
.select('*')
.eq('user_id', userId);
return {
profile: userData.data,
preferences: preferences.data,
export_date: new Date().toISOString(),
format: 'JSON'
};
}
// Consent management
static async updateConsent(userId: string, consentData: any) {
const { data, error } = await supabase
.from('user_consents')
.upsert({
user_id: userId,
consent_type: consentData.type,
granted: consentData.granted,
granted_at: consentData.granted ? new Date() : null,
updated_at: new Date()
});
if (error) throw error;
return data;
}
}
// Data retention policy
const applyDataRetentionPolicy = async () => {
const retentionPeriod = 7 * 365 * 24 * 60 * 60 * 1000; // 7 years
const cutoffDate = new Date(Date.now() - retentionPeriod);
// Delete old audit logs (except those marked for legal hold)
await supabase
.from('audit_logs')
.delete()
.lt('timestamp', cutoffDate.toISOString())
.eq('legal_hold', false);
};8. Security Monitoring and Alerts
Real-time Security Monitoring
Implement real-time monitoring for security threats:
// Security monitoring system
class SecurityMonitor {
private static suspiciousActivities: Map<string, number> = new Map();
static async detectSuspiciousActivity(userId: string, activity: string) {
const key = `${userId}-${activity}`;
const count = this.suspiciousActivities.get(key) || 0;
this.suspiciousActivities.set(key, count + 1);
// Alert if too many failed attempts
if (count > 5) {
await this.triggerSecurityAlert(userId, activity, count);
}
}
static async triggerSecurityAlert(userId: string, activity: string, count: number) {
// Log security alert
await supabase
.from('security_alerts')
.insert({
user_id: userId,
alert_type: 'suspicious_activity',
activity,
count,
severity: count > 10 ? 'high' : 'medium',
timestamp: new Date()
});
// Send notification to admins
await this.notifyAdmins(userId, activity, count);
}
static async notifyAdmins(userId: string, activity: string, count: number) {
const { data: admins } = await supabase
.from('user_profiles')
.select('email')
.eq('role', 'admin');
// Send email notifications to admins
for (const admin of admins || []) {
await sendSecurityAlertEmail(admin.email, {
userId,
activity,
count,
timestamp: new Date()
});
}
}
}
// Monitor login attempts
const monitorLoginAttempts = async (userId: string, success: boolean) => {
if (!success) {
await SecurityMonitor.detectSuspiciousActivity(userId, 'failed_login');
}
};
// Monitor admin actions
const monitorAdminActions = async (adminId: string, action: string) => {
const sensitiveActions = ['DELETE_USER', 'CHANGE_ROLE', 'EXPORT_DATA'];
if (sensitiveActions.includes(action)) {
await SecurityMonitor.detectSuspiciousActivity(adminId, action);
}
};Conclusion
Security in admin dashboards is not a one-time implementation but an ongoing process. By implementing these comprehensive security practices, you'll create a robust, secure admin dashboard that protects both your data and your users.
Remember to regularly audit your security measures, stay updated with the latest security best practices, and continuously monitor for potential threats. Security is a shared responsibility that requires constant vigilance.
Security Checklist
- ✅ Implement multi-factor authentication for all admin users
- ✅ Configure comprehensive RLS policies
- ✅ Set up rate limiting and input validation
- ✅ Encrypt sensitive data at rest and in transit
- ✅ Implement detailed audit logging
- ✅ Configure proper security headers
- ✅ Ensure GDPR compliance
- ✅ Set up real-time security monitoring
- ✅ Regular security audits and updates
- ✅ Incident response procedures