diff --git a/apps/api/prisma/seed-safe.ts b/apps/api/prisma/seed-safe.ts new file mode 100644 index 0000000..e09681d --- /dev/null +++ b/apps/api/prisma/seed-safe.ts @@ -0,0 +1,253 @@ +import { PrismaClient } from '@prisma/client'; +import { hashPassword } from '../src/utils/password'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Starting safe database seed...'); + console.log('ā„¹ļø This script will only create data that doesn\'t exist yet'); + + // Check if users already exist + const existingUsers = await prisma.user.count(); + console.log(`Found ${existingUsers} existing users`); + + if (existingUsers === 0) { + console.log('Creating default users...'); + + // Create admin user (password: admin123) + const adminPassword = await hashPassword('admin123'); + const admin = await prisma.user.create({ + data: { + username: 'admin', + email: 'admin@example.com', + password: adminPassword, + fullName: 'System Administrator', + role: 'admin', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin', + phone: '+84 123 456 789', + timezone: 'Asia/Ho_Chi_Minh', + language: 'vi', + lastLogin: new Date(), + profile: { + create: { + bio: 'System administrator with full access', + }, + }, + }, + }); + + // Create moderator user (password: operator123) + const operatorPassword = await hashPassword('operator123'); + const operator = await prisma.user.create({ + data: { + username: 'operator', + email: 'operator@example.com', + password: operatorPassword, + fullName: 'System Operator', + role: 'moderator', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=operator', + phone: '+84 987 654 321', + timezone: 'Asia/Ho_Chi_Minh', + language: 'en', + lastLogin: new Date(Date.now() - 86400000), // 1 day ago + profile: { + create: { + bio: 'System operator', + }, + }, + }, + }); + + // Create viewer user (password: viewer123) + const viewerPassword = await hashPassword('viewer123'); + const viewer = await prisma.user.create({ + data: { + username: 'viewer', + email: 'viewer@example.com', + password: viewerPassword, + fullName: 'Read Only User', + role: 'viewer', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=viewer', + timezone: 'Asia/Singapore', + language: 'en', + lastLogin: new Date(Date.now() - 172800000), // 2 days ago + profile: { + create: { + bio: 'Read-only access user', + }, + }, + }, + }); + + console.log('āœ… Default users created successfully!'); + + // Create sample activity logs for new admin user + console.log('Creating initial activity logs...'); + await prisma.activityLog.createMany({ + data: [ + { + userId: admin.id, + action: 'User logged in', + type: 'login', + ip: '192.168.1.100', + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + timestamp: new Date(Date.now() - 3600000), // 1 hour ago + success: true, + }, + { + userId: admin.id, + action: 'System initialized', + type: 'system', + ip: '192.168.1.100', + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + timestamp: new Date(), + details: 'Initial system setup completed', + success: true, + }, + ], + }); + } else { + console.log('ā„¹ļø Users already exist, skipping user creation'); + } + + // Check and create ModSecurity CRS rules if they don't exist + const existingCRSRules = await prisma.modSecCRSRule.count(); + console.log(`Found ${existingCRSRules} existing CRS rules`); + + if (existingCRSRules === 0) { + console.log('Creating ModSecurity CRS rules...'); + + // Create OWASP CRS rule configurations (metadata only) + await prisma.modSecCRSRule.createMany({ + data: [ + { + ruleFile: 'REQUEST-942-APPLICATION-ATTACK-SQLI.conf', + name: 'SQL Injection Protection', + category: 'SQLi', + description: 'Detects SQL injection attempts using OWASP CRS detection rules', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-941-APPLICATION-ATTACK-XSS.conf', + name: 'XSS Attack Prevention', + category: 'XSS', + description: 'Blocks cross-site scripting attacks', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-932-APPLICATION-ATTACK-RCE.conf', + name: 'RCE Detection', + category: 'RCE', + description: 'Remote code execution prevention', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-930-APPLICATION-ATTACK-LFI.conf', + name: 'LFI Protection', + category: 'LFI', + description: 'Local file inclusion prevention', + enabled: false, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf', + name: 'Session Fixation', + category: 'SESSION-FIXATION', + description: 'Prevents session fixation attacks', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-933-APPLICATION-ATTACK-PHP.conf', + name: 'PHP Attacks', + category: 'PHP', + description: 'PHP-specific attack prevention', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-920-PROTOCOL-ENFORCEMENT.conf', + name: 'Protocol Attacks', + category: 'PROTOCOL-ATTACK', + description: 'HTTP protocol attack prevention', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'RESPONSE-950-DATA-LEAKAGES.conf', + name: 'Data Leakage', + category: 'DATA-LEAKAGES', + description: 'Prevents sensitive data leakage', + enabled: false, + paranoia: 1 + }, + { + ruleFile: 'REQUEST-934-APPLICATION-ATTACK-GENERIC.conf', + name: 'SSRF Protection', + category: 'SSRF', + description: 'Server-side request forgery prevention (part of generic attacks)', + enabled: true, + paranoia: 1 + }, + { + ruleFile: 'RESPONSE-955-WEB-SHELLS.conf', + name: 'Web Shell Detection', + category: 'WEB-SHELL', + description: 'Detects web shell uploads', + enabled: true, + paranoia: 1 + }, + ], + }); + + console.log('āœ… ModSecurity CRS rules created successfully!'); + } else { + console.log('ā„¹ļø CRS rules already exist, skipping CRS rule creation'); + } + + console.log('\nāœ… Safe database seed completed successfully!'); + console.log('ā„¹ļø All existing data has been preserved'); + + // Show current user count + const totalUsers = await prisma.user.count(); + const totalCRSRules = await prisma.modSecCRSRule.count(); + console.log(`\nšŸ“Š Current database state:`); + console.log(` • Users: ${totalUsers}`); + console.log(` • CRS Rules: ${totalCRSRules}`); + + if (existingUsers === 0) { + console.log('\nšŸ“ Default Test Credentials (only if created):'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('Admin:'); + console.log(' Username: admin'); + console.log(' Password: admin123'); + console.log(' Email: admin@example.com'); + console.log(' Role: admin'); + console.log('\nOperator:'); + console.log(' Username: operator'); + console.log(' Password: operator123'); + console.log(' Email: operator@example.com'); + console.log(' Role: moderator'); + console.log('\nViewer:'); + console.log(' Username: viewer'); + console.log(' Password: viewer123'); + console.log(' Email: viewer@example.com'); + console.log(' Role: viewer'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } +} + +main() + .catch((e) => { + console.error('āŒ Error seeding database:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/apps/web/src/components/pages/Account.tsx b/apps/web/src/components/pages/Account.tsx index d9b1033..be43d35 100644 --- a/apps/web/src/components/pages/Account.tsx +++ b/apps/web/src/components/pages/Account.tsx @@ -29,12 +29,11 @@ import { Loader2 } from "lucide-react"; import { UserProfile, ActivityLog } from "@/types"; -import { useToast } from "@/hooks/use-toast"; +import { toast } from "sonner"; import { accountService } from "@/services/auth.service"; const Account = () => { const { t } = useTranslation(); - const { toast } = useToast(); const [profile, setProfile] = useState(null); const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); @@ -89,10 +88,8 @@ const Account = () => { setProfile(data); setTwoFactorEnabled(data.twoFactorEnabled); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Failed to load profile", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Failed to load profile" }); } }; @@ -110,43 +107,35 @@ const Account = () => { try { const updatedProfile = await accountService.updateProfile(profileForm); setProfile(updatedProfile); - toast({ - title: "Profile updated", + toast.success("Profile updated", { description: "Your profile information has been updated successfully" }); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Failed to update profile", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Failed to update profile" }); } }; const handlePasswordChange = async () => { if (passwordForm.newPassword !== passwordForm.confirmPassword) { - toast({ - title: "Password mismatch", - description: "New password and confirm password do not match", - variant: "destructive" + toast.error("Password mismatch", { + description: "New password and confirm password do not match" }); return; } if (passwordForm.newPassword.length < 8) { - toast({ - title: "Weak password", - description: "Password must be at least 8 characters long", - variant: "destructive" + toast.error("Weak password", { + description: "Password must be at least 8 characters long" }); return; } try { await accountService.changePassword(passwordForm); - toast({ - title: "Password changed", - description: "Your password has been changed successfully" + toast.success("āœ… Password Changed Successfully", { + description: "Your password has been updated. Please login again with your new password." }); setPasswordForm({ currentPassword: "", @@ -154,10 +143,8 @@ const Account = () => { confirmPassword: "" }); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Failed to change password", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Failed to change password" }); } }; @@ -172,15 +159,12 @@ const Account = () => { await accountService.disable2FA(password); setTwoFactorEnabled(false); setTwoFactorSetup(null); - toast({ - title: "2FA disabled", - description: "Two-factor authentication has been disabled" + toast.warning("āš ļø 2FA Disabled", { + description: "Two-factor authentication has been disabled for your account." }); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Failed to disable 2FA", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Failed to disable 2FA" }); } } else { @@ -188,15 +172,12 @@ const Account = () => { try { const setup = await accountService.setup2FA(); setTwoFactorSetup(setup); - toast({ - title: "2FA Setup", - description: "Scan the QR code with your authenticator app" + toast.info("šŸ“± 2FA Setup Ready", { + description: "Scan the QR code with your authenticator app to complete setup." }); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Failed to setup 2FA", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Failed to setup 2FA" }); } } @@ -204,10 +185,8 @@ const Account = () => { const handleVerify2FA = async () => { if (!verificationToken || verificationToken.length !== 6) { - toast({ - title: "Invalid token", - description: "Please enter a 6-digit code", - variant: "destructive" + toast.error("Invalid token", { + description: "Please enter a 6-digit code" }); return; } @@ -217,25 +196,21 @@ const Account = () => { setTwoFactorEnabled(true); setTwoFactorSetup(null); setVerificationToken(""); - toast({ - title: "2FA enabled", - description: "Two-factor authentication has been enabled successfully" + toast.success("šŸ›”ļø 2FA Enabled Successfully", { + description: "Two-factor authentication is now active. Your account is more secure!" }); loadProfile(); } catch (error: any) { - toast({ - title: "Error", - description: error.response?.data?.message || "Invalid verification code", - variant: "destructive" + toast.error("Error", { + description: error.response?.data?.message || "Invalid verification code" }); } }; const copyBackupCode = (code: string) => { navigator.clipboard.writeText(code); - toast({ - title: "Copied", - description: "Backup code copied to clipboard" + toast.success("šŸ“‹ Code Copied", { + description: "Backup code has been copied to your clipboard." }); }; diff --git a/scripts/update.sh b/scripts/update.sh index 140b2eb..a96a88a 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -132,10 +132,10 @@ log "Running database migrations..." cd "${BACKEND_DIR}" pnpm prisma migrate deploy >> "$LOG_FILE" 2>&1 || error "Failed to run migrations" -# Seed database -log "Seeding database..." +# Seed database safely (only create missing data, preserve existing) +log "Seeding database safely..." cd "${BACKEND_DIR}" -pnpm ts-node prisma/seed.ts >> "$LOG_FILE" 2>&1 || warn "Failed to seed database (this is normal if data already exists)" +pnpm ts-node prisma/seed-safe.ts >> "$LOG_FILE" 2>&1 || warn "Failed to seed database safely" # Build backend log "Building backend..." @@ -250,7 +250,7 @@ log "" log "šŸ“‹ Updated Components:" log " • Backend API: Rebuilt and restarted" log " • Frontend UI: Rebuilt and restarted" -log " • Database: Migrations applied, new tables seeded" +log " • Database: Migrations applied, missing data created (existing data preserved)" log "" log "🌐 Services Status:" log " • Backend API: http://${PUBLIC_IP}:3001"