diff --git a/frontend/src/components/ChangePasswordForm.tsx b/frontend/src/components/ChangePasswordForm.tsx index 8b2db83..58d56d8 100644 --- a/frontend/src/components/ChangePasswordForm.tsx +++ b/frontend/src/components/ChangePasswordForm.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ChangePasswordCredentials } from '../types'; import { changePassword } from '../services/authService'; +import { validatePasswordStrength } from '../utils/passwordValidation'; interface ChangePasswordFormProps { onSuccess?: () => void; @@ -18,6 +19,7 @@ const ChangePasswordForm: React.FC = ({ onSuccess, onCa const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const [passwordErrors, setPasswordErrors] = useState([]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -25,6 +27,12 @@ const ChangePasswordForm: React.FC = ({ onSuccess, onCa setConfirmPassword(value); } else { setFormData(prev => ({ ...prev, [name]: value })); + + // Validate password strength on change for new password + if (name === 'newPassword') { + const validation = validatePasswordStrength(value); + setPasswordErrors(validation.errors); + } } }; @@ -32,6 +40,14 @@ const ChangePasswordForm: React.FC = ({ onSuccess, onCa e.preventDefault(); setError(null); + // Validate password strength + const validation = validatePasswordStrength(formData.newPassword); + if (!validation.isValid) { + setError(t('auth.passwordStrengthError')); + setPasswordErrors(validation.errors); + return; + } + // Validate passwords match if (formData.newPassword !== confirmPassword) { setError(t('auth.passwordsNotMatch')); @@ -100,8 +116,24 @@ const ChangePasswordForm: React.FC = ({ onSuccess, onCa value={formData.newPassword} onChange={handleChange} required - minLength={6} + minLength={8} /> + {/* Password strength hints */} + {formData.newPassword && passwordErrors.length > 0 && ( +
+

{t('auth.passwordStrengthHint')}

+
    + {passwordErrors.map((errorKey) => ( +
  • + {t(`auth.${errorKey}`)} +
  • + ))} +
+
+ )} + {formData.newPassword && passwordErrors.length === 0 && ( +

✓ {t('auth.passwordStrengthHint')}

+ )}
@@ -116,7 +148,7 @@ const ChangePasswordForm: React.FC = ({ onSuccess, onCa value={confirmPassword} onChange={handleChange} required - minLength={6} + minLength={8} />
diff --git a/frontend/src/components/ui/DefaultPasswordWarningModal.tsx b/frontend/src/components/ui/DefaultPasswordWarningModal.tsx new file mode 100644 index 0000000..1b69d6b --- /dev/null +++ b/frontend/src/components/ui/DefaultPasswordWarningModal.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +interface DefaultPasswordWarningModalProps { + isOpen: boolean; + onClose: () => void; +} + +const DefaultPasswordWarningModal: React.FC = ({ + isOpen, + onClose, +}) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + if (!isOpen) return null; + + const handleGoToSettings = () => { + onClose(); + navigate('/settings'); + // Auto-scroll to password section after a small delay to ensure page is loaded + setTimeout(() => { + const passwordSection = document.querySelector('[data-section="password"]'); + if (passwordSection) { + passwordSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // If the section is collapsed, expand it + const clickTarget = passwordSection.querySelector('[role="button"]'); + if (clickTarget && !passwordSection.querySelector('.mt-4')) { + (clickTarget as HTMLElement).click(); + } + } + }, 100); + }; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + return ( +
+
+
+
+
+ + + +
+
+

+ {t('auth.defaultPasswordWarning')} +

+

+ {t('auth.defaultPasswordMessage')} +

+
+
+ +
+ + +
+
+
+
+ ); +}; + +export default DefaultPasswordWarningModal; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index b42bd7f..c5db75c 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -14,12 +14,12 @@ const initialState: AuthState = { // Create auth context const AuthContext = createContext<{ auth: AuthState; - login: (username: string, password: string) => Promise; + login: (username: string, password: string) => Promise<{ success: boolean; isUsingDefaultPassword?: boolean }>; register: (username: string, password: string, isAdmin?: boolean) => Promise; logout: () => void; }>({ auth: initialState, - login: async () => false, + login: async () => ({ success: false }), register: async () => false, logout: () => { }, }); @@ -90,7 +90,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => }, []); // Login function - const login = async (username: string, password: string): Promise => { + const login = async (username: string, password: string): Promise<{ success: boolean; isUsingDefaultPassword?: boolean }> => { try { const response = await authService.login({ username, password }); @@ -101,14 +101,17 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => user: response.user, error: null, }); - return true; + return { + success: true, + isUsingDefaultPassword: response.isUsingDefaultPassword, + }; } else { setAuth({ ...initialState, loading: false, error: response.message || 'Authentication failed', }); - return false; + return { success: false }; } } catch (error) { setAuth({ @@ -116,7 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => loading: false, error: 'Authentication failed', }); - return false; + return { success: false }; } }; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 745684a..47b9700 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useAuth } from '../contexts/AuthContext'; import ThemeSwitch from '@/components/ui/ThemeSwitch'; import LanguageSwitch from '@/components/ui/LanguageSwitch'; +import DefaultPasswordWarningModal from '@/components/ui/DefaultPasswordWarningModal'; const LoginPage: React.FC = () => { const { t } = useTranslation(); @@ -11,6 +12,7 @@ const LoginPage: React.FC = () => { const [password, setPassword] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [showDefaultPasswordWarning, setShowDefaultPasswordWarning] = useState(false); const { login } = useAuth(); const navigate = useNavigate(); @@ -26,10 +28,15 @@ const LoginPage: React.FC = () => { return; } - const success = await login(username, password); + const result = await login(username, password); - if (success) { - navigate('/'); + if (result.success) { + if (result.isUsingDefaultPassword) { + // Show warning modal instead of navigating immediately + setShowDefaultPasswordWarning(true); + } else { + navigate('/'); + } } else { setError(t('auth.loginFailed')); } @@ -40,6 +47,11 @@ const LoginPage: React.FC = () => { } }; + const handleCloseWarning = () => { + setShowDefaultPasswordWarning(false); + navigate('/'); + }; + return (
{/* Top-right controls */} @@ -138,6 +150,12 @@ const LoginPage: React.FC = () => {
+ + {/* Default Password Warning Modal */} + ); }; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index b841023..0e1449a 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -794,10 +794,11 @@ const SettingsPage: React.FC = () => { {/* Change Password */} -
+
toggleSection('password')} + role="button" >

{t('auth.changePassword')}

{sectionsVisible.password ? '▼' : '►'} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index db09ee6..2f46032 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -359,6 +359,7 @@ export interface AuthResponse { token?: string; user?: IUser; message?: string; + isUsingDefaultPassword?: boolean; } // Official Registry types (from registry.modelcontextprotocol.io) diff --git a/frontend/src/utils/passwordValidation.ts b/frontend/src/utils/passwordValidation.ts new file mode 100644 index 0000000..a7488f3 --- /dev/null +++ b/frontend/src/utils/passwordValidation.ts @@ -0,0 +1,38 @@ +/** + * Frontend password strength validation utility + * Should match backend validation rules + */ + +export interface PasswordValidationResult { + isValid: boolean; + errors: string[]; +} + +export const validatePasswordStrength = (password: string): PasswordValidationResult => { + const errors: string[] = []; + + // Check minimum length + if (password.length < 8) { + errors.push('passwordMinLength'); + } + + // Check for at least one letter + if (!/[a-zA-Z]/.test(password)) { + errors.push('passwordRequireLetter'); + } + + // Check for at least one number + if (!/\d/.test(password)) { + errors.push('passwordRequireNumber'); + } + + // Check for at least one special character + if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) { + errors.push('passwordRequireSpecial'); + } + + return { + isValid: errors.length === 0, + errors, + }; +}; diff --git a/locales/en.json b/locales/en.json index ab25cd3..20080c5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -69,7 +69,16 @@ "changePasswordError": "Failed to change password", "changePassword": "Change Password", "passwordChanged": "Password changed successfully", - "passwordChangeError": "Failed to change password" + "passwordChangeError": "Failed to change password", + "defaultPasswordWarning": "Default Password Security Warning", + "defaultPasswordMessage": "You are using the default password (admin123), which poses a security risk. Please change your password immediately to protect your account.", + "goToSettings": "Go to Settings", + "passwordStrengthError": "Password does not meet security requirements", + "passwordMinLength": "Password must be at least 8 characters long", + "passwordRequireLetter": "Password must contain at least one letter", + "passwordRequireNumber": "Password must contain at least one number", + "passwordRequireSpecial": "Password must contain at least one special character", + "passwordStrengthHint": "Password must be at least 8 characters and contain letters, numbers, and special characters" }, "server": { "addServer": "Add Server", diff --git a/locales/fr.json b/locales/fr.json index 27a4c69..793b90b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -69,7 +69,16 @@ "changePasswordError": "Échec du changement de mot de passe", "changePassword": "Changer le mot de passe", "passwordChanged": "Mot de passe changé avec succès", - "passwordChangeError": "Échec du changement de mot de passe" + "passwordChangeError": "Échec du changement de mot de passe", + "defaultPasswordWarning": "Avertissement de sécurité du mot de passe par défaut", + "defaultPasswordMessage": "Vous utilisez le mot de passe par défaut (admin123), ce qui présente un risque de sécurité. Veuillez changer votre mot de passe immédiatement pour protéger votre compte.", + "goToSettings": "Aller aux paramètres", + "passwordStrengthError": "Le mot de passe ne répond pas aux exigences de sécurité", + "passwordMinLength": "Le mot de passe doit contenir au moins 8 caractères", + "passwordRequireLetter": "Le mot de passe doit contenir au moins une lettre", + "passwordRequireNumber": "Le mot de passe doit contenir au moins un chiffre", + "passwordRequireSpecial": "Le mot de passe doit contenir au moins un caractère spécial", + "passwordStrengthHint": "Le mot de passe doit contenir au moins 8 caractères et inclure des lettres, des chiffres et des caractères spéciaux" }, "server": { "addServer": "Ajouter un serveur", diff --git a/locales/zh.json b/locales/zh.json index c5e22a5..261b8e9 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -69,7 +69,16 @@ "changePasswordError": "修改密码失败", "changePassword": "修改密码", "passwordChanged": "密码修改成功", - "passwordChangeError": "修改密码失败" + "passwordChangeError": "修改密码失败", + "defaultPasswordWarning": "默认密码安全警告", + "defaultPasswordMessage": "您正在使用默认密码(admin123),这存在安全风险。为了保护您的账户安全,请立即修改密码。", + "goToSettings": "前往修改", + "passwordStrengthError": "密码不符合安全要求", + "passwordMinLength": "密码长度至少为 8 个字符", + "passwordRequireLetter": "密码必须包含至少一个字母", + "passwordRequireNumber": "密码必须包含至少一个数字", + "passwordRequireSpecial": "密码必须包含至少一个特殊字符", + "passwordStrengthHint": "密码必须至少 8 个字符,且包含字母、数字和特殊字符" }, "server": { "addServer": "添加服务器", diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index c2917cc..01c0cd1 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -10,6 +10,8 @@ import { import { getDataService } from '../services/services.js'; import { DataService } from '../services/dataService.js'; import { JWT_SECRET } from '../config/jwt.js'; +import { validatePasswordStrength, isDefaultPassword } from '../utils/passwordValidation.js'; +import { getPackageVersion } from '../utils/version.js'; const dataService: DataService = getDataService(); @@ -64,6 +66,14 @@ export const login = async (req: Request, res: Response): Promise => { }, }; + // Check if user is admin with default password + const version = getPackageVersion(); + const isUsingDefaultPassword = + user.username === 'admin' && + user.isAdmin && + isDefaultPassword(password) && + version !== 'dev'; + jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY }, (err, token) => { if (err) throw err; res.json({ @@ -75,6 +85,7 @@ export const login = async (req: Request, res: Response): Promise => { isAdmin: user.isAdmin, permissions: dataService.getPermissions(user), }, + isUsingDefaultPassword, }); }); } catch (error) { @@ -172,6 +183,17 @@ export const changePassword = async (req: Request, res: Response): Promise const username = (req as any).user.username; try { + // Validate new password strength + const validationResult = validatePasswordStrength(newPassword); + if (!validationResult.isValid) { + res.status(400).json({ + success: false, + message: 'Password does not meet security requirements', + errors: validationResult.errors, + }); + return; + } + // Find user by username const user = findUserByUsername(username); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index bdd87b7..f84ad97 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -10,6 +10,7 @@ import { getAdminCount, } from '../services/userService.js'; import { loadSettings } from '../config/index.js'; +import { validatePasswordStrength } from '../utils/passwordValidation.js'; // Admin permission check middleware function const requireAdmin = (req: Request, res: Response): boolean => { @@ -100,6 +101,17 @@ export const createUser = async (req: Request, res: Response): Promise => return; } + // Validate password strength + const validationResult = validatePasswordStrength(password); + if (!validationResult.isValid) { + res.status(400).json({ + success: false, + message: 'Password does not meet security requirements', + errors: validationResult.errors, + }); + return; + } + const newUser = await createNewUser(username, password, isAdmin || false); if (!newUser) { res.status(400).json({ @@ -163,7 +175,19 @@ export const updateExistingUser = async (req: Request, res: Response): Promise { + const errors: string[] = []; + + // Check minimum length + if (password.length < 8) { + errors.push('Password must be at least 8 characters long'); + } + + // Check for at least one letter + if (!/[a-zA-Z]/.test(password)) { + errors.push('Password must contain at least one letter'); + } + + // Check for at least one number + if (!/\d/.test(password)) { + errors.push('Password must contain at least one number'); + } + + // Check for at least one special character + if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) { + errors.push('Password must contain at least one special character'); + } + + return { + isValid: errors.length === 0, + errors, + }; +}; + +/** + * Check if a password is the default password (admin123) + */ +export const isDefaultPassword = (plainPassword: string): boolean => { + return plainPassword === 'admin123'; +};