Add frontend authentication - Landing page, Dashboard, Auth context

This commit is contained in:
oguz ozturk 2026-01-10 16:23:40 +03:00
parent 4676200874
commit ec0164691b
8 changed files with 722 additions and 112 deletions

View File

@ -13,7 +13,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.2",
"react-router-dom": "^6.20.1"
"react-router-dom": "^6.20.1",
"@heroicons/react": "^2.1.1"
},
"devDependencies": {
"@types/react": "^18.2.43",

View File

@ -1,93 +1,45 @@
import { useState } from 'react'
import DomainSetup from './pages/DomainSetup'
import DomainSetupNew from './pages/DomainSetupNew'
import DomainList from './pages/DomainList'
import AdminCFAccounts from './pages/AdminCFAccounts'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import Landing from './pages/Landing'
import Dashboard from './pages/Dashboard'
import './App.css'
// Protected route wrapper
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth()
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
)
}
return isAuthenticated ? children : <Navigate to="/" replace />
}
function App() {
const [currentPage, setCurrentPage] = useState('setup-new')
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Header */}
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
<span className="text-white text-xl font-bold">H</span>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Hosting Platform</h1>
<p className="text-sm text-gray-500">DNS & SSL Management</p>
</div>
</div>
<nav className="flex space-x-4">
<button
onClick={() => setCurrentPage('setup-new')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'setup-new'
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
🆕 Add Domain (New)
</button>
<button
onClick={() => setCurrentPage('setup')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'setup'
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Add Domain (Old)
</button>
<button
onClick={() => setCurrentPage('list')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'list'
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
My Domains
</button>
<button
onClick={() => setCurrentPage('admin')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'admin'
? 'bg-purple-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Admin
</button>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{currentPage === 'setup-new' && <DomainSetupNew />}
{currentPage === 'setup' && <DomainSetup />}
{currentPage === 'list' && <DomainList />}
{currentPage === 'admin' && <AdminCFAccounts />}
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="text-center text-gray-500 text-sm">
<p>© 2024 Hosting Platform. Powered by Cloudflare.</p>
<p className="mt-1">Automated DNS & SSL Management</p>
</div>
</div>
</footer>
</div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<Landing />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AuthProvider>
</BrowserRouter>
)
}

View File

@ -0,0 +1,109 @@
/**
* Auth Context - Global authentication state management
*/
import { createContext, useContext, useState, useEffect } from 'react';
import { authAPI } from '../services/api';
const AuthContext = createContext(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [customer, setCustomer] = useState(null);
const [loading, setLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Check if user is logged in on mount
useEffect(() => {
const checkAuth = async () => {
const token = localStorage.getItem('auth_token');
if (token) {
try {
const response = await authAPI.getProfile();
setUser(response.data.user);
setCustomer(response.data.customer);
setIsAuthenticated(true);
} catch (error) {
console.error('Auth check failed:', error);
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
}
}
setLoading(false);
};
checkAuth();
}, []);
const login = async (email, password) => {
try {
const response = await authAPI.login({ email, password });
const { token, user: userData, customer: customerData } = response.data;
localStorage.setItem('auth_token', token);
localStorage.setItem('user', JSON.stringify(userData));
setUser(userData);
setCustomer(customerData);
setIsAuthenticated(true);
return { success: true, data: response.data };
} catch (error) {
console.error('Login failed:', error);
return {
success: false,
error: error.response?.data?.message || 'Login failed',
};
}
};
const register = async (data) => {
try {
const response = await authAPI.register(data);
const { token, user: userData, customer: customerData } = response.data;
localStorage.setItem('auth_token', token);
localStorage.setItem('user', JSON.stringify(userData));
setUser(userData);
setCustomer(customerData);
setIsAuthenticated(true);
return { success: true, data: response.data };
} catch (error) {
console.error('Registration failed:', error);
return {
success: false,
error: error.response?.data?.message || 'Registration failed',
};
}
};
const logout = () => {
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
setUser(null);
setCustomer(null);
setIsAuthenticated(false);
};
const value = {
user,
customer,
loading,
isAuthenticated,
login,
register,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

View File

@ -1,33 +1,35 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
@layer base {
* {
@apply border-gray-200;
}
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
body {
@apply bg-gray-50 text-gray-900 font-sans;
}
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
width: 100%;
margin: 0 auto;
text-align: center;
@layer components {
/* Custom component styles */
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
}
.btn-secondary {
@apply bg-secondary-500 hover:bg-secondary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
}
.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;
}
.card {
@apply bg-white rounded-xl shadow-sm border border-gray-200 p-6;
}
}

View File

@ -0,0 +1,231 @@
/**
* Customer Dashboard - Main dashboard with sidebar navigation
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import {
HomeIcon,
GlobeAltIcon,
ServerIcon,
WifiIcon,
ShieldCheckIcon,
Cog6ToothIcon,
ArrowRightOnRectangleIcon,
} from '@heroicons/react/24/outline';
const Dashboard = () => {
const { user, customer, logout } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('overview');
const handleLogout = () => {
logout();
navigate('/');
};
const menuItems = [
{ id: 'overview', name: 'Overview', icon: HomeIcon },
{ id: 'dns', name: 'DNS Management', icon: GlobeAltIcon },
{ id: 'containers', name: 'Containers', icon: ServerIcon },
{ id: 'network', name: 'Network', icon: WifiIcon },
{ id: 'security', name: 'Security', icon: ShieldCheckIcon },
{ id: 'settings', name: 'Settings', icon: Cog6ToothIcon },
];
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
{/* Logo */}
<div className="p-6 border-b border-gray-200">
<img
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
alt="ARGE ICT"
className="h-10"
/>
</div>
{/* User info */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center text-white font-semibold">
{user?.full_name?.charAt(0) || 'U'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user?.full_name}
</p>
<p className="text-xs text-gray-500 truncate">{customer?.company_name || user?.email}</p>
</div>
</div>
<div className="mt-3 flex items-center justify-between text-xs">
<span className="text-gray-500">Plan:</span>
<span className="px-2 py-1 bg-primary-100 text-primary-700 rounded-full font-medium">
{customer?.subscription_plan || 'Free'}
</span>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
isActive
? 'bg-primary-50 text-primary-700'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
<Icon className="w-5 h-5" />
<span className="font-medium">{item.name}</span>
</button>
);
})}
</nav>
{/* Logout */}
<div className="p-4 border-t border-gray-200">
<button
onClick={handleLogout}
className="w-full flex items-center space-x-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<ArrowRightOnRectangleIcon className="w-5 h-5" />
<span className="font-medium">Logout</span>
</button>
</div>
</aside>
{/* Main content */}
<main className="flex-1 overflow-auto">
<div className="p-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
{menuItems.find((item) => item.id === activeTab)?.name}
</h1>
<p className="text-gray-600 mt-1">
Welcome back, {user?.full_name}!
</p>
</div>
{/* Content based on active tab */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Stats cards */}
<div className="card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Domains</p>
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
<p className="text-xs text-gray-500 mt-1">
of {customer?.max_domains} limit
</p>
</div>
<GlobeAltIcon className="w-12 h-12 text-primary-500" />
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Containers</p>
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
<p className="text-xs text-gray-500 mt-1">
of {customer?.max_containers} limit
</p>
</div>
<ServerIcon className="w-12 h-12 text-secondary-500" />
</div>
</div>
<div className="card">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Status</p>
<p className="text-3xl font-bold text-green-600 mt-1">Active</p>
<p className="text-xs text-gray-500 mt-1">All systems operational</p>
</div>
<ShieldCheckIcon className="w-12 h-12 text-green-500" />
</div>
</div>
</div>
)}
{activeTab === 'dns' && (
<div className="card">
<p className="text-gray-600">DNS Management module coming soon...</p>
</div>
)}
{activeTab === 'containers' && (
<div className="card">
<p className="text-gray-600">Container management module coming soon...</p>
</div>
)}
{activeTab === 'network' && (
<div className="card">
<p className="text-gray-600">Network configuration module coming soon...</p>
</div>
)}
{activeTab === 'security' && (
<div className="card">
<p className="text-gray-600">Security settings module coming soon...</p>
</div>
)}
{activeTab === 'settings' && (
<div className="card">
<h2 className="text-xl font-semibold mb-4">Account Settings</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
value={user?.email || ''}
disabled
className="input-field bg-gray-50"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
type="text"
value={user?.full_name || ''}
disabled
className="input-field bg-gray-50"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name
</label>
<input
type="text"
value={customer?.company_name || ''}
disabled
className="input-field bg-gray-50"
/>
</div>
</div>
</div>
)}
</div>
</main>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,237 @@
/**
* Landing Page - Register/Login with animations
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const Landing = () => {
const [isLogin, setIsLogin] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();
const { login, register } = useAuth();
// Form states
const [formData, setFormData] = useState({
email: '',
password: '',
password_confirm: '',
full_name: '',
company_name: '',
});
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
setError('');
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
let result;
if (isLogin) {
result = await login(formData.email, formData.password);
} else {
result = await register(formData);
}
if (result.success) {
navigate('/dashboard');
} else {
setError(result.error);
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-secondary-50 flex items-center justify-center p-4">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-primary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-brand-green-light/20 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
</div>
<div className="w-full max-w-md relative z-10">
{/* Logo */}
<div className="text-center mb-8">
<img
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
alt="ARGE ICT"
className="h-16 mx-auto mb-4"
/>
<h1 className="text-3xl font-bold text-gray-900">Hosting Platform</h1>
<p className="text-gray-600 mt-2">
Professional WordPress hosting with container infrastructure
</p>
</div>
{/* Form Card */}
<div className="card">
{/* Tabs */}
<div className="flex border-b border-gray-200 mb-6">
<button
onClick={() => {
setIsLogin(true);
setError('');
}}
className={`flex-1 py-3 text-center font-medium transition-colors ${
isLogin
? 'text-primary-600 border-b-2 border-primary-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Login
</button>
<button
onClick={() => {
setIsLogin(false);
setError('');
}}
className={`flex-1 py-3 text-center font-medium transition-colors ${
!isLogin
? 'text-primary-600 border-b-2 border-primary-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Register
</button>
</div>
{/* Error message */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{error}
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
{!isLogin && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name *
</label>
<input
type="text"
name="full_name"
value={formData.full_name}
onChange={handleChange}
className="input-field"
required={!isLogin}
placeholder="John Doe"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Company Name
</label>
<input
type="text"
name="company_name"
value={formData.company_name}
onChange={handleChange}
className="input-field"
placeholder="Acme Inc (optional)"
/>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className="input-field"
required
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password *
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="input-field"
required
placeholder="••••••••"
minLength={8}
/>
</div>
{!isLogin && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Confirm Password *
</label>
<input
type="password"
name="password_confirm"
value={formData.password_confirm}
onChange={handleChange}
className="input-field"
required={!isLogin}
placeholder="••••••••"
minLength={8}
/>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Please wait...' : isLogin ? 'Login' : 'Create Account'}
</button>
</form>
</div>
{/* Footer */}
<p className="text-center text-sm text-gray-600 mt-6">
© 2026 ARGE ICT. All rights reserved.
</p>
</div>
<style>{`
@keyframes blob {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
`}</style>
</div>
);
};
export default Landing;

View File

@ -9,6 +9,42 @@ const api = axios.create({
},
})
// Request interceptor - Add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor - Handle errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('auth_token')
localStorage.removeItem('user')
window.location.href = '/'
}
return Promise.reject(error)
}
)
// Auth API
export const authAPI = {
register: (data) => api.post('/api/auth/register', data),
login: (data) => api.post('/api/auth/login', data),
getProfile: () => api.get('/api/auth/me'),
verifyToken: (token) => api.post('/api/auth/verify-token', { token }),
}
export const dnsAPI = {
// Health check
health: () => api.get('/health'),

View File

@ -5,7 +5,49 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
extend: {
colors: {
// Brand colors from ARGE ICT logo
brand: {
green: {
DEFAULT: '#159052',
dark: '#046D3F',
light: '#53BA6F',
},
orange: '#F69036',
blue: '#0F578B',
red: '#B42832',
},
// Semantic colors
primary: {
50: '#e6f7ef',
100: '#b3e6d0',
200: '#80d5b1',
300: '#4dc492',
400: '#1ab373',
500: '#159052', // Main brand green
600: '#117342',
700: '#0d5631',
800: '#093921',
900: '#051c10',
},
secondary: {
50: '#fff3e6',
100: '#ffdbb3',
200: '#ffc380',
300: '#ffab4d',
400: '#ff931a',
500: '#F69036', // Brand orange
600: '#c5722b',
700: '#945520',
800: '#633816',
900: '#321c0b',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}