From 1b1d2651f3180e5b688c305fbd0a8fdced91f0d0 Mon Sep 17 00:00:00 2001 From: oguz ozturk Date: Sun, 11 Jan 2026 18:03:26 +0300 Subject: [PATCH] feat: Update UI with ARGE ICT branding and add profile management - Add ARGE ICT color palette to Tailwind config - Add company logo to login and sidebar - Remove default credentials from login page (security) - Add profile management page for admins - Add password change functionality - Update all UI components with new color scheme - Add backend endpoints for profile update and password change --- backend/app/routes/auth.py | 85 ++++++++- frontend/src/App.jsx | 2 + frontend/src/components/Layout.jsx | 27 ++- frontend/src/context/AuthContext.jsx | 7 +- frontend/src/index.css | 56 +++--- frontend/src/pages/Login.jsx | 26 +-- frontend/src/pages/Profile.jsx | 257 +++++++++++++++++++++++++++ frontend/tailwind.config.js | 69 +++++-- 8 files changed, 473 insertions(+), 56 deletions(-) create mode 100644 frontend/src/pages/Profile.jsx diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 05e33be..decfb79 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -110,9 +110,92 @@ def logout(current_admin): ) db.session.add(log) db.session.commit() - + return jsonify({ 'status': 'success', 'message': 'Logged out successfully' }), 200 +@auth_bp.route('/profile', methods=['PUT']) +@token_required +def update_profile(current_admin): + """Update admin profile""" + try: + data = request.get_json() + + # Update allowed fields + if 'full_name' in data: + current_admin.full_name = data['full_name'] + + if 'email' in data: + # Check if email is already taken by another admin + existing = AdminUser.query.filter_by(email=data['email']).first() + if existing and existing.id != current_admin.id: + return jsonify({'error': 'Email already in use'}), 400 + current_admin.email = data['email'] + + db.session.commit() + + # Log action + log = AuditLog( + admin_id=current_admin.id, + action='update_profile', + details=f"Updated profile information", + ip_address=request.remote_addr + ) + db.session.add(log) + db.session.commit() + + return jsonify({ + 'status': 'success', + 'message': 'Profile updated successfully', + 'admin': current_admin.to_dict() + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + +@auth_bp.route('/change-password', methods=['POST']) +@token_required +def change_password(current_admin): + """Change admin password""" + try: + data = request.get_json() + current_password = data.get('current_password') + new_password = data.get('new_password') + + if not current_password or not new_password: + return jsonify({'error': 'Current and new password required'}), 400 + + # Verify current password + if not current_admin.check_password(current_password): + return jsonify({'error': 'Current password is incorrect'}), 401 + + # Validate new password + if len(new_password) < 8: + return jsonify({'error': 'New password must be at least 8 characters'}), 400 + + # Update password + current_admin.set_password(new_password) + db.session.commit() + + # Log action + log = AuditLog( + admin_id=current_admin.id, + action='change_password', + details='Password changed successfully', + ip_address=request.remote_addr + ) + db.session.add(log) + db.session.commit() + + return jsonify({ + 'status': 'success', + 'message': 'Password changed successfully' + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e39c9c0..ac08b6b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import Dashboard from './pages/Dashboard' import Plans from './pages/Plans' import CFAccounts from './pages/CFAccounts' import Customers from './pages/Customers' +import Profile from './pages/Profile' import PrivateRoute from './components/PrivateRoute' function App() { @@ -17,6 +18,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 97458d9..2814576 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -22,12 +22,18 @@ const Layout = ({ children }) => {
{/* Sidebar */} {/* Main Content */} -
+
{children}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index b975bb7..d185f6d 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -57,8 +57,13 @@ export const AuthProvider = ({ children }) => { } }; + const updateAdmin = (updatedAdmin) => { + localStorage.setItem('admin_user', JSON.stringify(updatedAdmin)); + setAdmin(updatedAdmin); + }; + return ( - + {children} ); diff --git a/frontend/src/index.css b/frontend/src/index.css index 0303fd1..d63c5b9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -10,55 +10,67 @@ @layer components { .btn-primary { - @apply px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium; + @apply px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md; } - + .btn-secondary { - @apply px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-medium; + @apply px-4 py-2 bg-secondary-500 text-white rounded-lg hover:bg-secondary-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md; } - + .btn-danger { - @apply px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium; + @apply px-4 py-2 bg-danger-500 text-white rounded-lg hover:bg-danger-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md; } - + + .btn-accent { + @apply px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md; + } + + .btn-outline { + @apply px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-all duration-200 font-medium; + } + .input-field { - @apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all; + @apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all duration-200; } - + .card { - @apply bg-white rounded-lg shadow-sm border border-gray-200 p-6; + @apply bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200; } - + .table { @apply w-full border-collapse; } - + .table th { - @apply bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider px-6 py-3 border-b border-gray-200; + @apply bg-primary-50 text-left text-xs font-medium text-primary-700 uppercase tracking-wider px-6 py-3 border-b border-primary-200; } - + .table td { @apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 border-b border-gray-200; } - + .badge { @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; } - + .badge-success { - @apply bg-green-100 text-green-800; + @apply bg-success-100 text-success-800; } - + .badge-warning { - @apply bg-yellow-100 text-yellow-800; + @apply bg-accent-100 text-accent-800; } - + .badge-danger { - @apply bg-red-100 text-red-800; + @apply bg-danger-100 text-danger-800; } - + .badge-info { - @apply bg-blue-100 text-blue-800; + @apply bg-primary-100 text-primary-800; + } + + .badge-secondary { + @apply bg-secondary-100 text-secondary-800; } } diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 4dc66c3..e0328d1 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -33,17 +33,25 @@ const Login = () => { }; return ( -
+
-
+
-

Admin Panel

+ {/* ARGE ICT Logo */} +
+ ARGE ICT +
+

Admin Panel

Hosting Platform Management

{error && ( -
-

{error}

+
+

{error}

)} @@ -85,14 +93,6 @@ const Login = () => { {loading ? 'Logging in...' : 'Login'} - -
-

- Default credentials:
- Username: admin
- Password: admin123 -

-
diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx new file mode 100644 index 0000000..15638cf --- /dev/null +++ b/frontend/src/pages/Profile.jsx @@ -0,0 +1,257 @@ +import { useState } from 'react'; +import { useAuth } from '../context/AuthContext'; +import api from '../services/api'; + +const Profile = () => { + const { admin, updateAdmin } = useAuth(); + const [activeTab, setActiveTab] = useState('profile'); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState({ type: '', text: '' }); + + // Profile form state + const [profileData, setProfileData] = useState({ + full_name: admin?.full_name || '', + email: admin?.email || '', + }); + + // Password form state + const [passwordData, setPasswordData] = useState({ + current_password: '', + new_password: '', + confirm_password: '', + }); + + const handleProfileUpdate = async (e) => { + e.preventDefault(); + setLoading(true); + setMessage({ type: '', text: '' }); + + try { + const response = await api.put('/auth/profile', profileData); + updateAdmin(response.data.admin); + setMessage({ type: 'success', text: 'Profile updated successfully!' }); + } catch (error) { + setMessage({ + type: 'error', + text: error.response?.data?.error || 'Failed to update profile' + }); + } finally { + setLoading(false); + } + }; + + const handlePasswordChange = async (e) => { + e.preventDefault(); + setLoading(true); + setMessage({ type: '', text: '' }); + + // Validate passwords match + if (passwordData.new_password !== passwordData.confirm_password) { + setMessage({ type: 'error', text: 'New passwords do not match' }); + setLoading(false); + return; + } + + // Validate password length + if (passwordData.new_password.length < 8) { + setMessage({ type: 'error', text: 'Password must be at least 8 characters' }); + setLoading(false); + return; + } + + try { + await api.post('/auth/change-password', { + current_password: passwordData.current_password, + new_password: passwordData.new_password, + }); + setMessage({ type: 'success', text: 'Password changed successfully!' }); + setPasswordData({ + current_password: '', + new_password: '', + confirm_password: '', + }); + } catch (error) { + setMessage({ + type: 'error', + text: error.response?.data?.error || 'Failed to change password' + }); + } finally { + setLoading(false); + } + }; + + return ( +
+

Account Settings

+ + {/* Tabs */} +
+ +
+ + {/* Message Alert */} + {message.text && ( +
+ {message.text} +
+ )} + + {/* Profile Tab */} + {activeTab === 'profile' && ( +
+

Profile Information

+
+
+ + +

Username cannot be changed

+
+ +
+ + setProfileData({ ...profileData, full_name: e.target.value })} + className="input-field" + required + /> +
+ +
+ + setProfileData({ ...profileData, email: e.target.value })} + className="input-field" + required + /> +
+ +
+ + +
+ +
+ +
+
+
+ )} + + {/* Password Tab */} + {activeTab === 'password' && ( +
+

Change Password

+
+
+ + setPasswordData({ ...passwordData, current_password: e.target.value })} + className="input-field" + required + /> +
+ +
+ + setPasswordData({ ...passwordData, new_password: e.target.value })} + className="input-field" + required + minLength={8} + /> +

Minimum 8 characters

+
+ +
+ + setPasswordData({ ...passwordData, confirm_password: e.target.value })} + className="input-field" + required + minLength={8} + /> +
+ +
+ +
+
+
+ )} +
+ ); +}; + +export default Profile; + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index cd3cae4..e0faa69 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,17 +7,66 @@ export default { theme: { extend: { colors: { + // ARGE ICT Brand Colors primary: { - 50: '#eff6ff', - 100: '#dbeafe', - 200: '#bfdbfe', - 300: '#93c5fd', - 400: '#60a5fa', - 500: '#3b82f6', - 600: '#2563eb', - 700: '#1d4ed8', - 800: '#1e40af', - 900: '#1e3a8a', + 50: '#e6f2ff', + 100: '#cce5ff', + 200: '#99cbff', + 300: '#66b0ff', + 400: '#3396ff', + 500: '#0F578B', // Main blue + 600: '#0c4670', + 700: '#093554', + 800: '#062438', + 900: '#03121c', + }, + secondary: { + 50: '#e6f7f0', + 100: '#ccefe0', + 200: '#99dfc1', + 300: '#66cfa2', + 400: '#33bf83', + 500: '#159052', // Main green + 600: '#117342', + 700: '#0d5631', + 800: '#083921', + 900: '#041d10', + }, + accent: { + 50: '#fef3e6', + 100: '#fde7cc', + 200: '#fbcf99', + 300: '#f9b766', + 400: '#f79f33', + 500: '#F69036', // Orange + 600: '#c5732b', + 700: '#945620', + 800: '#623a16', + 900: '#311d0b', + }, + danger: { + 50: '#fce8ea', + 100: '#f9d1d4', + 200: '#f3a3a9', + 300: '#ed757e', + 400: '#e74753', + 500: '#B42832', // Main red + 600: '#902028', + 700: '#6c181e', + 800: '#481014', + 900: '#24080a', + }, + success: { + 50: '#e9f7f0', + 100: '#d3efe1', + 200: '#a7dfc3', + 300: '#7bcfa5', + 400: '#4fbf87', + 500: '#046D3F', // Dark green + 600: '#035732', + 700: '#024126', + 800: '#022c19', + 900: '#01160d', }, }, },