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
This commit is contained in:
oguz ozturk 2026-01-11 18:03:26 +03:00
parent 9a2745cd92
commit 1b1d2651f3
8 changed files with 473 additions and 56 deletions

View File

@ -116,3 +116,86 @@ def logout(current_admin):
'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

View File

@ -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() {
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>

View File

@ -22,12 +22,18 @@ const Layout = ({ children }) => {
<div className="min-h-screen flex">
{/* Sidebar */}
<aside className="w-64 bg-white border-r border-gray-200">
<div className="p-6">
<h1 className="text-2xl font-bold text-primary-600">Admin Panel</h1>
<p className="text-sm text-gray-500 mt-1">Hosting Platform</p>
<div className="p-6 border-b border-gray-200">
<div className="flex items-center gap-3 mb-2">
<img
src="/arge-logo.svg"
alt="ARGE ICT"
className="h-10 w-auto"
/>
</div>
<p className="text-sm text-gray-500">Admin Panel</p>
</div>
<nav className="px-4 space-y-1">
<nav className="px-4 py-4 space-y-1">
{menuItems.map((item) => {
const isActive = location.pathname === item.path;
return (
@ -47,8 +53,11 @@ const Layout = ({ children }) => {
})}
</nav>
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200">
<div className="flex items-center gap-3 mb-3">
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200 bg-white">
<Link
to="/profile"
className="flex items-center gap-3 mb-3 p-2 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
<span className="text-primary-600 font-semibold">
{admin?.username?.charAt(0).toUpperCase()}
@ -60,10 +69,10 @@ const Layout = ({ children }) => {
</p>
<p className="text-xs text-gray-500 truncate">{admin?.email}</p>
</div>
</div>
</Link>
<button
onClick={handleLogout}
className="w-full btn-secondary text-sm"
className="w-full btn-outline text-sm"
>
Logout
</button>
@ -71,7 +80,7 @@ const Layout = ({ children }) => {
</aside>
{/* Main Content */}
<main className="flex-1 overflow-auto">
<main className="flex-1 overflow-auto bg-gray-50">
<div className="p-8">{children}</div>
</main>
</div>

View File

@ -57,8 +57,13 @@ export const AuthProvider = ({ children }) => {
}
};
const updateAdmin = (updatedAdmin) => {
localStorage.setItem('admin_user', JSON.stringify(updatedAdmin));
setAdmin(updatedAdmin);
};
return (
<AuthContext.Provider value={{ admin, login, logout, loading }}>
<AuthContext.Provider value={{ admin, login, logout, updateAdmin, loading }}>
{children}
</AuthContext.Provider>
);

View File

@ -10,23 +10,31 @@
@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 {
@ -34,7 +42,7 @@
}
.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 {
@ -46,19 +54,23 @@
}
.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;
}
}

View File

@ -33,17 +33,25 @@ const Login = () => {
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-secondary-50 to-primary-100">
<div className="w-full max-w-md">
<div className="card">
<div className="card shadow-2xl">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Admin Panel</h1>
{/* ARGE ICT Logo */}
<div className="flex justify-center mb-6">
<img
src="/arge-logo.svg"
alt="ARGE ICT"
className="h-16 w-auto"
/>
</div>
<h1 className="text-3xl font-bold text-primary-500 mb-2">Admin Panel</h1>
<p className="text-gray-600">Hosting Platform Management</p>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800 text-sm">{error}</p>
<div className="mb-4 p-4 bg-danger-50 border border-danger-200 rounded-lg">
<p className="text-danger-800 text-sm">{error}</p>
</div>
)}
@ -85,14 +93,6 @@ const Login = () => {
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-xs text-blue-800">
<strong>Default credentials:</strong><br />
Username: admin<br />
Password: admin123
</p>
</div>
</div>
</div>
</div>

View File

@ -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 (
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Account Settings</h1>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('profile')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'profile'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Profile Information
</button>
<button
onClick={() => setActiveTab('password')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'password'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Change Password
</button>
</nav>
</div>
{/* Message Alert */}
{message.text && (
<div className={`mb-6 p-4 rounded-lg ${
message.type === 'success'
? 'bg-success-50 border border-success-200 text-success-800'
: 'bg-danger-50 border border-danger-200 text-danger-800'
}`}>
{message.text}
</div>
)}
{/* Profile Tab */}
{activeTab === 'profile' && (
<div className="card">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Profile Information</h2>
<form onSubmit={handleProfileUpdate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input
type="text"
value={admin?.username || ''}
disabled
className="input-field bg-gray-50 cursor-not-allowed"
/>
<p className="mt-1 text-sm text-gray-500">Username cannot be changed</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
type="text"
value={profileData.full_name}
onChange={(e) => setProfileData({ ...profileData, full_name: e.target.value })}
className="input-field"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
type="email"
value={profileData.email}
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
className="input-field"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Role
</label>
<input
type="text"
value={admin?.role || ''}
disabled
className="input-field bg-gray-50 cursor-not-allowed"
/>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={loading}
className="btn-primary disabled:opacity-50"
>
{loading ? 'Updating...' : 'Update Profile'}
</button>
</div>
</form>
</div>
)}
{/* Password Tab */}
{activeTab === 'password' && (
<div className="card">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Change Password</h2>
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Current Password
</label>
<input
type="password"
value={passwordData.current_password}
onChange={(e) => setPasswordData({ ...passwordData, current_password: e.target.value })}
className="input-field"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input
type="password"
value={passwordData.new_password}
onChange={(e) => setPasswordData({ ...passwordData, new_password: e.target.value })}
className="input-field"
required
minLength={8}
/>
<p className="mt-1 text-sm text-gray-500">Minimum 8 characters</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm New Password
</label>
<input
type="password"
value={passwordData.confirm_password}
onChange={(e) => setPasswordData({ ...passwordData, confirm_password: e.target.value })}
className="input-field"
required
minLength={8}
/>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={loading}
className="btn-primary disabled:opacity-50"
>
{loading ? 'Changing...' : 'Change Password'}
</button>
</div>
</form>
</div>
)}
</div>
);
};
export default Profile;

View File

@ -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',
},
},
},