Upgraded to 1.1.0

1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
This commit is contained in:
Hosteroid
2025-10-09 18:02:46 +03:00
parent adc28b97f0
commit e5b9599755
61 changed files with 6838 additions and 812 deletions

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?? 'Authentication' ?> - Domain Monitor</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#4A90E2',
dark: '#357ABD',
light: '#6BA3E8',
}
}
}
}
}
</script>
<style>
body {
background-color: #f8f9fa;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full">
<!-- Auth Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<?= $content ?>
</div>
<!-- Footer -->
<div class="text-center mt-6">
<p class="text-gray-500 text-xs">
© <?= date('Y') ?> Domain Monitor. All rights reserved.
</p>
</div>
</div>
<?php if (isset($scripts)): ?>
<?= $scripts ?>
<?php endif; ?>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<?php
$title = 'Forgot Password';
ob_start();
?>
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
<i class="fas fa-key text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Forgot Password?</h1>
<p class="text-sm text-gray-500">No worries, we'll send you reset instructions</p>
</div>
<!-- Error/Success Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="mb-6 bg-green-50 border border-green-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm text-green-700"><?= htmlspecialchars($_SESSION['success']) ?></span>
</div>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<!-- Forgot Password Form -->
<form method="POST" action="/forgot-password" class="space-y-5">
<!-- Email Field -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1.5">
Email Address
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400 text-sm"></i>
</div>
<input
type="email"
id="email"
name="email"
required
autofocus
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your email address">
</div>
<p class="text-xs text-gray-500 mt-1">Enter the email associated with your account</p>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm">
<i class="fas fa-paper-plane mr-2"></i>
Send Reset Link
</button>
</form>
<!-- Back to Login Link -->
<div class="text-center mt-6 pt-6 border-t border-gray-200">
<a href="/login" class="inline-flex items-center text-sm text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left mr-2"></i>
Back to Login
</a>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/base-auth.php';
?>

View File

@@ -1,156 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Domain Monitor</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#4A90E2',
dark: '#357ABD',
light: '#6BA3E8',
}
}
}
}
}
</script>
<style>
body {
background-color: #f8f9fa;
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-md w-full">
<!-- Login Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
<i class="fas fa-globe text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Welcome Back</h1>
<p class="text-sm text-gray-500">Sign in to access your account</p>
</div>
<?php
$title = 'Login';
ob_start();
?>
<!-- Error Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
<i class="fas fa-globe text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Welcome Back</h1>
<p class="text-sm text-gray-500">Sign in to access your account</p>
</div>
<!-- Login Form -->
<form method="POST" action="/login" class="space-y-5">
<!-- Username Field -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
Username
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400 text-sm"></i>
</div>
<input
type="text"
id="username"
name="username"
required
autofocus
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your username">
</div>
</div>
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1.5">
Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password"
name="password"
required
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your password">
<button
type="button"
onclick="togglePassword()"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon"></i>
</button>
</div>
</div>
<!-- Remember Me -->
<div class="flex items-center justify-between">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
name="remember"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
<span class="ml-2 text-sm text-gray-600">Remember me</span>
</label>
<a href="#" class="text-sm text-primary hover:text-primary-dark">
Forgot password?
</a>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm">
<i class="fas fa-sign-in-alt mr-2"></i>
Sign In
</button>
</form>
<!-- Error Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<!-- Footer -->
<div class="text-center mt-6">
<p class="text-gray-500 text-xs">
© <?= date('Y') ?> Domain Monitor. All rights reserved.
</p>
<!-- Login Form -->
<form method="POST" action="/login" class="space-y-5">
<!-- Username Field -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
Username
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400 text-sm"></i>
</div>
<input
type="text"
id="username"
name="username"
required
autofocus
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your username">
</div>
</div>
<script>
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1.5">
Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password"
name="password"
required
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your password">
<button
type="button"
onclick="togglePassword()"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon"></i>
</button>
</div>
</div>
<!-- Remember Me -->
<div class="flex items-center justify-between">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
name="remember"
value="1"
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
<span class="ml-2 text-sm text-gray-600">Remember me</span>
</label>
<a href="/forgot-password" class="text-sm text-primary hover:text-primary-dark">
Forgot password?
</a>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm">
<i class="fas fa-sign-in-alt mr-2"></i>
Sign In
</button>
</form>
<?php if ($registrationEnabled ?? false): ?>
<!-- Sign Up Link -->
<div class="text-center mt-6 pt-6 border-t border-gray-200">
<p class="text-sm text-gray-600">
Don't have an account?
<a href="/register" class="text-primary hover:text-primary-dark font-medium">
Create Account
</a>
</p>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
$scripts = <<<'SCRIPT'
<script>
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.getElementById('toggleIcon');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
</script>
</body>
</html>
}
</script>
SCRIPT;
require __DIR__ . '/base-auth.php';
?>

217
app/Views/auth/register.php Normal file
View File

@@ -0,0 +1,217 @@
<?php
$title = 'Register';
ob_start();
?>
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
<i class="fas fa-user-plus text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Create Account</h1>
<p class="text-sm text-gray-500">Join Domain Monitor today</p>
</div>
<!-- Error/Success Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="mb-6 bg-green-50 border border-green-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm text-green-700"><?= htmlspecialchars($_SESSION['success']) ?></span>
</div>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<!-- Registration Form -->
<form method="POST" action="/register" class="space-y-4">
<!-- Full Name Field -->
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 mb-1.5">
Full Name
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400 text-sm"></i>
</div>
<input
type="text"
id="full_name"
name="full_name"
required
autofocus
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter your full name">
</div>
</div>
<!-- Username Field -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
Username
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-at text-gray-400 text-sm"></i>
</div>
<input
type="text"
id="username"
name="username"
required
pattern="[a-zA-Z0-9_]+"
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Choose a username">
</div>
<p class="text-xs text-gray-500 mt-1">Letters, numbers, and underscores only</p>
</div>
<!-- Email Field -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1.5">
Email Address
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400 text-sm"></i>
</div>
<input
type="email"
id="email"
name="email"
required
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="your.email@example.com">
</div>
</div>
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1.5">
Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password"
name="password"
required
minlength="8"
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Create a strong password">
<button
type="button"
onclick="togglePassword('password')"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon-password"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
</div>
<!-- Confirm Password Field -->
<div>
<label for="password_confirm" class="block text-sm font-medium text-gray-700 mb-1.5">
Confirm Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password_confirm"
name="password_confirm"
required
minlength="8"
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Re-enter your password">
<button
type="button"
onclick="togglePassword('password_confirm')"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon-password_confirm"></i>
</button>
</div>
</div>
<!-- Terms Checkbox -->
<div class="flex items-start pt-2">
<div class="flex items-center h-5">
<input
type="checkbox"
id="terms"
name="terms"
required
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
</div>
<label for="terms" class="ml-2 text-xs text-gray-600">
I agree to the Terms of Service and Privacy Policy
</label>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm mt-6">
<i class="fas fa-user-plus mr-2"></i>
Create Account
</button>
</form>
<!-- Sign In Link -->
<div class="text-center mt-6 pt-6 border-t border-gray-200">
<p class="text-sm text-gray-600">
Already have an account?
<a href="/login" class="text-primary hover:text-primary-dark font-medium">
Sign In
</a>
</p>
</div>
<?php
$content = ob_get_clean();
$scripts = <<<'SCRIPT'
<script>
function togglePassword(fieldId) {
const passwordInput = document.getElementById(fieldId);
const toggleIcon = document.getElementById('toggleIcon-' + fieldId);
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
}
// Client-side password match validation
document.querySelector('form').addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password_confirm').value;
if (password !== passwordConfirm) {
e.preventDefault();
alert('Passwords do not match!');
}
});
</script>
SCRIPT;
require __DIR__ . '/base-auth.php';
?>

View File

@@ -0,0 +1,147 @@
<?php
$title = 'Reset Password';
ob_start();
?>
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-14 h-14 bg-primary rounded-lg mb-4">
<i class="fas fa-lock-open text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-1">Reset Password</h1>
<p class="text-sm text-gray-500">Enter your new password below</p>
</div>
<!-- Error/Success Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<!-- Reset Password Form -->
<form method="POST" action="/reset-password" class="space-y-4">
<!-- Hidden token field -->
<input type="hidden" name="token" value="<?= htmlspecialchars($token ?? '') ?>">
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1.5">
New Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password"
name="password"
required
minlength="8"
autofocus
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Enter new password">
<button
type="button"
onclick="togglePassword('password')"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon-password"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
</div>
<!-- Confirm Password Field -->
<div>
<label for="password_confirm" class="block text-sm font-medium text-gray-700 mb-1.5">
Confirm New Password
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input
type="password"
id="password_confirm"
name="password_confirm"
required
minlength="8"
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Re-enter new password">
<button
type="button"
onclick="togglePassword('password_confirm')"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none">
<i class="fas fa-eye text-sm" id="toggleIcon-password_confirm"></i>
</button>
</div>
</div>
<!-- Password Strength Indicator -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p class="text-xs text-blue-800 mb-2">
<i class="fas fa-shield-alt mr-1"></i>
<strong>Password Requirements:</strong>
</p>
<ul class="text-xs text-blue-700 space-y-1 ml-5 list-disc">
<li>At least 8 characters long</li>
<li>Mix of uppercase and lowercase letters recommended</li>
<li>Include numbers and special characters for extra security</li>
</ul>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors duration-200 flex items-center justify-center text-sm mt-6">
<i class="fas fa-check mr-2"></i>
Reset Password
</button>
</form>
<!-- Back to Login Link -->
<div class="text-center mt-6 pt-6 border-t border-gray-200">
<a href="/login" class="inline-flex items-center text-sm text-gray-600 hover:text-gray-800">
<i class="fas fa-arrow-left mr-2"></i>
Back to Login
</a>
</div>
<?php
$content = ob_get_clean();
$scripts = <<<'SCRIPT'
<script>
function togglePassword(fieldId) {
const passwordInput = document.getElementById(fieldId);
const toggleIcon = document.getElementById('toggleIcon-' + fieldId);
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
passwordInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
}
}
// Client-side password match validation
document.querySelector('form').addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password_confirm').value;
if (password !== passwordConfirm) {
e.preventDefault();
alert('Passwords do not match!');
}
});
</script>
SCRIPT;
require __DIR__ . '/base-auth.php';
?>

View File

@@ -0,0 +1,79 @@
<?php
$title = 'Verify Email';
ob_start();
?>
<?php if ($verified ?? false): ?>
<!-- Success State -->
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">
<i class="fas fa-check-circle text-green-600 text-3xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-2">Email Verified!</h1>
<p class="text-gray-600 mb-6">Your email address has been successfully verified.</p>
<a href="/login" class="inline-flex items-center px-6 py-2.5 bg-primary hover:bg-primary-dark text-white text-sm rounded-lg transition-colors font-medium">
<i class="fas fa-sign-in-alt mr-2"></i>
Sign In to Your Account
</a>
</div>
<?php elseif ($error ?? false): ?>
<!-- Error State -->
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4">
<i class="fas fa-times-circle text-red-600 text-3xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-2">Verification Failed</h1>
<p class="text-gray-600 mb-6"><?= htmlspecialchars($errorMessage ?? 'Invalid or expired verification link.') ?></p>
<div class="space-y-2">
<a href="/login" class="block text-center px-6 py-2.5 bg-primary hover:bg-primary-dark text-white text-sm rounded-lg transition-colors font-medium">
<i class="fas fa-sign-in-alt mr-2"></i>
Go to Login
</a>
<a href="/resend-verification" class="block text-center px-6 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm rounded-lg transition-colors font-medium">
<i class="fas fa-redo mr-2"></i>
Resend Verification Email
</a>
</div>
</div>
<?php else: ?>
<!-- Pending State -->
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
<i class="fas fa-envelope text-blue-600 text-3xl"></i>
</div>
<h1 class="text-2xl font-semibold text-gray-900 mb-2">Check Your Email</h1>
<p class="text-gray-600 mb-6">
We've sent a verification link to <strong><?= htmlspecialchars($email ?? 'your email') ?></strong>.
Please check your inbox and click the link to verify your account.
</p>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 text-left">
<p class="text-sm text-blue-800 mb-2">
<i class="fas fa-info-circle mr-1"></i>
<strong>Didn't receive the email?</strong>
</p>
<ul class="text-xs text-blue-700 space-y-1 ml-5 list-disc">
<li>Check your spam or junk folder</li>
<li>Make sure you entered the correct email address</li>
<li>Wait a few minutes for the email to arrive</li>
</ul>
</div>
<div class="space-y-2">
<a href="/resend-verification" class="block text-center px-6 py-2.5 bg-primary hover:bg-primary-dark text-white text-sm rounded-lg transition-colors font-medium">
<i class="fas fa-redo mr-2"></i>
Resend Verification Email
</a>
<a href="/login" class="block text-center text-sm text-gray-600 hover:text-gray-800">
Back to Login
</a>
</div>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
require __DIR__ . '/base-auth.php';
?>

View File

@@ -104,19 +104,12 @@ ob_start();
</div>
<div class="flex items-center space-x-2 flex-shrink-0">
<?php
$status = $domain['status'] ?? 'active';
$statusClasses = [
'active' => 'bg-green-100 text-green-700',
'expiring_soon' => 'bg-orange-100 text-orange-700',
'expired' => 'bg-red-100 text-red-700',
'error' => 'bg-red-100 text-red-700',
'available' => 'bg-blue-100 text-blue-700'
];
$statusClass = $statusClasses[$status] ?? 'bg-gray-100 text-gray-700';
$statusLabel = $status === 'expiring_soon' ? 'Expiring Soon' : ($status === 'available' ? 'Available' : ucfirst($status));
// Display data prepared by DomainHelper in controller
$statusClass = $domain['statusClass'];
$statusText = $domain['statusText'];
?>
<span class="px-2 py-1 rounded text-xs font-medium <?= $statusClass ?>">
<?= $statusLabel ?>
<?= $statusText ?>
</span>
<a href="/domains/<?= $domain['id'] ?>" class="text-gray-400 hover:text-primary">
<i class="fas fa-chevron-right text-sm"></i>
@@ -238,7 +231,8 @@ ob_start();
<div class="p-4 space-y-2">
<?php foreach ($expiringThisMonth as $domain): ?>
<?php
$daysLeft = floor((strtotime($domain['expiration_date']) - time()) / 86400);
// Display data prepared by DomainHelper in controller
$daysLeft = $domain['daysLeft'];
$urgencyClass = $daysLeft <= 7 ? 'text-red-600' : ($daysLeft <= 30 ? 'text-orange-600' : 'text-yellow-600');
?>
<div class="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:border-gray-300 hover:shadow-sm transition-all duration-200">

View File

@@ -132,11 +132,15 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<?php endforeach; ?>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<div class="flex items-end space-x-2">
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-filter mr-2"></i>
Apply Filters
</button>
<a href="/domains" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
@@ -217,73 +221,12 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($domains as $domain): ?>
<?php
// Calculate days until expiry and determine status color
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
$expiryClass = '';
if ($daysLeft !== null) {
if ($daysLeft < 0) {
$expiryClass = 'text-red-600 font-semibold';
} elseif ($daysLeft <= 30) {
$expiryClass = 'text-orange-600 font-semibold';
} elseif ($daysLeft <= 90) {
$expiryClass = 'text-yellow-600';
}
}
// Recalculate domain status if it's empty or error (for backward compatibility)
$domainStatus = $domain['status'];
if (empty($domainStatus) || $domainStatus === 'error') {
$whoisData = json_decode($domain['whois_data'] ?? '{}', true);
$statusArray = $whoisData['status'] ?? [];
$isAvailable = false;
foreach ($statusArray as $status) {
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
$isAvailable = true;
break;
}
}
if ($isAvailable) {
$domainStatus = 'available';
} elseif ($daysLeft !== null) {
if ($daysLeft < 0) {
$domainStatus = 'expired';
} elseif ($daysLeft <= 30) {
$domainStatus = 'expiring_soon';
} else {
$domainStatus = 'active';
}
} else {
$domainStatus = 'error';
}
}
// Status badge color
if ($domainStatus === 'available') {
$statusClass = 'bg-blue-100 text-blue-700 border-blue-200';
$statusText = 'Available';
$statusIcon = 'fa-info-circle';
} elseif ($daysLeft !== null && $daysLeft <= 30 && $daysLeft >= 0) {
$statusClass = 'bg-orange-100 text-orange-700 border-orange-200';
$statusText = 'Expiring Soon';
$statusIcon = 'fa-exclamation-triangle';
} elseif ($domainStatus === 'active') {
$statusClass = 'bg-green-100 text-green-700 border-green-200';
$statusText = 'Active';
$statusIcon = 'fa-check-circle';
} elseif ($domainStatus === 'expired') {
$statusClass = 'bg-red-100 text-red-700 border-red-200';
$statusText = 'Expired';
$statusIcon = 'fa-times-circle';
} elseif ($domainStatus === 'error') {
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
$statusText = 'Error';
$statusIcon = 'fa-exclamation-circle';
} else {
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
$statusText = ucfirst($domainStatus);
$statusIcon = 'fa-times-circle';
}
// Display data prepared by DomainHelper in controller
$daysLeft = $domain['daysLeft'];
$expiryClass = $domain['expiryClass'];
$statusClass = $domain['statusClass'];
$statusText = $domain['statusText'];
$statusIcon = $domain['statusIcon'];
?>
<tr class="hover:bg-gray-50 transition-colors duration-150 domain-row">
<td class="px-4 py-4">

View File

@@ -3,44 +3,12 @@ $title = 'Domain Details';
$pageTitle = htmlspecialchars($domain['domain_name']);
$pageDescription = 'Domain information and monitoring status';
$pageIcon = 'fas fa-globe';
// Data already formatted by controller via DomainHelper
$whoisData = json_decode($domain['whois_data'] ?? '{}', true);
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
// Recalculate domain status if it's empty or error (for backward compatibility)
$domainStatus = $domain['status'];
if (empty($domainStatus) || $domainStatus === 'error') {
// Check WHOIS data for AVAILABLE status
$statusArray = $whoisData['status'] ?? [];
$isAvailable = false;
foreach ($statusArray as $status) {
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
$isAvailable = true;
break;
}
}
if ($isAvailable) {
$domainStatus = 'available';
} elseif ($daysLeft !== null) {
if ($daysLeft < 0) {
$domainStatus = 'expired';
} elseif ($daysLeft <= 30) {
$domainStatus = 'expiring_soon';
} else {
$domainStatus = 'active';
}
} else {
$domainStatus = 'error';
}
}
// Determine expiry color
$expiryColor = 'green';
if ($daysLeft !== null) {
if ($daysLeft < 0) $expiryColor = 'red';
elseif ($daysLeft <= 30) $expiryColor = 'orange';
elseif ($daysLeft <= 90) $expiryColor = 'yellow';
}
$daysLeft = $domain['daysLeft'];
$domainStatus = $domain['displayStatus'];
$expiryColor = $domain['expiryColor'];
ob_start();
?>
@@ -49,32 +17,10 @@ ob_start();
<div class="mb-3 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<?php
// Determine domain status badge
if ($domainStatus === 'available') {
$statusClass = 'bg-blue-100 text-blue-700 border-blue-200';
$statusText = 'Available (Not Registered)';
$statusIcon = 'fa-info-circle';
} elseif ($domainStatus === 'expired') {
$statusClass = 'bg-red-100 text-red-700 border-red-200';
$statusText = 'Expired';
$statusIcon = 'fa-times-circle';
} elseif ($domainStatus === 'expiring_soon' || ($daysLeft !== null && $daysLeft <= 30 && $daysLeft >= 0)) {
$statusClass = 'bg-orange-100 text-orange-700 border-orange-200';
$statusText = 'Expiring Soon';
$statusIcon = 'fa-exclamation-triangle';
} elseif ($domainStatus === 'active') {
$statusClass = 'bg-green-100 text-green-700 border-green-200';
$statusText = 'Active';
$statusIcon = 'fa-check-circle';
} elseif ($domainStatus === 'error') {
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
$statusText = 'Error';
$statusIcon = 'fa-exclamation-circle';
} else {
$statusClass = 'bg-gray-100 text-gray-700 border-gray-200';
$statusText = ucfirst($domainStatus);
$statusIcon = 'fa-question-circle';
}
// Status badge data prepared by DomainHelper in controller
$statusClass = $domain['statusClass'];
$statusText = $domain['statusText'];
$statusIcon = $domain['statusIcon'];
?>
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold <?= $statusClass ?>">
<i class="fas <?= $statusIcon ?> mr-1.5"></i>
@@ -257,51 +203,20 @@ ob_start();
<?php endif; ?>
<!-- Domain Status -->
<?php if (!empty($whoisData['status']) && is_array($whoisData['status'])): ?>
<?php
// Pre-filter to count only valid statuses
$validStatuses = [];
foreach ($whoisData['status'] as $status) {
$cleanStatus = trim($status);
// Skip if it's just a URL or starts with http/https or //
if (empty($cleanStatus) ||
strpos($cleanStatus, 'http') === 0 ||
strpos($cleanStatus, '//') === 0 ||
strpos($cleanStatus, 'www.') === 0) {
continue;
}
// Keep the full status text, don't split by spaces
// Skip if after cleaning it's empty or just a URL
if (empty($cleanStatus) || strpos($cleanStatus, 'http') === 0 || strpos($cleanStatus, '//') === 0) {
continue;
}
$validStatuses[] = $cleanStatus;
}
?>
<?php if (!empty($validStatuses)): ?>
<?php if (!empty($domain['parsedStatuses'])): ?>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-4 py-2 border-b border-gray-200 bg-gray-50">
<h3 class="text-xs font-semibold text-gray-700 uppercase tracking-wider flex items-center">
<i class="fas fa-info-circle text-gray-400 mr-2" style="font-size: 10px;"></i>
Domain Status (<?= count($validStatuses) ?>)
Domain Status (<?= count($domain['parsedStatuses']) ?>)
</h3>
</div>
<div class="p-4">
<div class="flex flex-wrap gap-1.5">
<?php foreach ($validStatuses as $cleanStatus): ?>
<?php foreach ($domain['parsedStatuses'] as $cleanStatus): ?>
<?php
// Convert to readable format
$readableStatus = $cleanStatus;
// Convert camelCase to readable format (for cases like "clientTransferProhibited")
$readableStatus = preg_replace('/([a-z])([A-Z])/', '$1 $2', $readableStatus);
// Convert underscores to spaces and capitalize words
$readableStatus = str_replace('_', ' ', $readableStatus);
$readableStatus = ucwords(strtolower($readableStatus));
// Format status text using helper
$readableStatus = \App\Helpers\DomainHelper::formatStatusText($cleanStatus);
?>
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs font-medium" title="<?= htmlspecialchars($cleanStatus) ?>">
<?= htmlspecialchars($readableStatus) ?>
@@ -310,7 +225,6 @@ ob_start();
</div>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
@@ -335,11 +249,8 @@ ob_start();
<div>
<p class="font-semibold text-sm text-gray-900"><?= htmlspecialchars($domain['group_name']) ?></p>
<?php if (!empty($domain['channels'])): ?>
<?php
$activeChannels = array_filter($domain['channels'], fn($ch) => $ch['is_active']);
?>
<p class="text-xs text-gray-600">
<?= count($activeChannels) ?> / <?= count($domain['channels']) ?> channels active
<?= $domain['activeChannelCount'] ?? 0 ?> / <?= count($domain['channels']) ?> channels active
</p>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Installation Complete</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#4A90E2', dark: '#357ABD' }
}
}
}
}
</script>
<style>
body { background-color: #f8f9fa; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-2xl w-full">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<!-- Success Icon -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4">
<i class="fas fa-check-circle text-green-600 text-5xl"></i>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">Installation Complete!</h1>
<p class="text-gray-600">Domain Monitor is ready to use</p>
</div>
<!-- Important Notice -->
<div class="bg-amber-50 border-2 border-amber-400 rounded-lg p-6 mb-6">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-amber-600 text-2xl mr-4"></i>
<div class="flex-1">
<h3 class="text-lg font-semibold text-amber-900 mb-2">Save Your Credentials!</h3>
<p class="text-sm text-amber-800 mb-4">This password will not be shown again. Save it to a secure password manager.</p>
<div class="bg-white rounded-lg border border-amber-300 p-4">
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600">Username:</span>
<span class="text-sm font-mono font-bold text-gray-900">admin</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-600">Password:</span>
<span class="text-sm font-mono font-bold text-gray-900 select-all"><?= htmlspecialchars($adminPassword ?? '********') ?></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Success Checklist -->
<div class="bg-gray-50 rounded-lg border border-gray-200 p-6 mb-6">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wider mb-4">Installation Summary</h3>
<div class="space-y-3">
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-3"></i>
<span class="text-sm text-gray-700">Database tables created</span>
</div>
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-3"></i>
<span class="text-sm text-gray-700">Admin account configured</span>
</div>
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-3"></i>
<span class="text-sm text-gray-700">Encryption key generated</span>
</div>
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-3"></i>
<span class="text-sm text-gray-700">All migrations applied</span>
</div>
</div>
</div>
<!-- Next Steps -->
<div class="bg-blue-50 rounded-lg border border-blue-200 p-4 mb-6">
<h3 class="text-sm font-semibold text-blue-900 mb-3">
<i class="fas fa-lightbulb mr-2"></i>Next Steps
</h3>
<ol class="text-sm text-blue-800 space-y-1 ml-5 list-decimal">
<li>Log in with your admin credentials</li>
<li>Configure email settings (Settings → Email)</li>
<li>Import TLD registry data (TLD Registry → Import TLDs)</li>
<li>Add your first domain</li>
<li>Set up notification groups</li>
<li>Configure cron job for automated monitoring</li>
</ol>
</div>
<a href="/login" class="block w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium text-center transition-colors">
<i class="fas fa-sign-in-alt mr-2"></i>
Go to Login
</a>
</div>
<div class="text-center mt-6">
<p class="text-gray-500 text-xs">© <?= date('Y') ?> Domain Monitor</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Update</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#4A90E2', dark: '#357ABD' }
}
}
}
}
</script>
<style>
body { background-color: #f8f9fa; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-2xl w-full">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<!-- Header -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-lg mb-4">
<i class="fas fa-arrow-up text-white text-3xl"></i>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">System Update</h1>
<p class="text-gray-600">New database migrations are available</p>
</div>
<!-- Warning -->
<div class="bg-amber-50 border border-amber-300 rounded-lg p-4 mb-6">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-amber-600 text-xl mr-3"></i>
<div>
<h3 class="font-semibold text-amber-900 mb-1">Backup Recommended</h3>
<p class="text-sm text-amber-800">Please backup your database before running updates.</p>
</div>
</div>
</div>
<!-- Pending Migrations -->
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3">Pending Migrations</h2>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<ul class="space-y-2">
<?php foreach ($migrations as $migration): ?>
<li class="flex items-center text-sm">
<i class="fas fa-circle text-xs text-gray-400 mr-3"></i>
<span class="font-mono text-gray-700"><?= htmlspecialchars($migration) ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="mt-3 pt-3 border-t border-gray-300">
<p class="text-sm font-semibold text-gray-900">
<i class="fas fa-database mr-2"></i>
Total: <?= count($migrations) ?> migration(s)
</p>
</div>
</div>
</div>
<!-- Error Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); endif; ?>
<!-- Actions -->
<form method="POST" action="/install/update" class="space-y-3">
<button type="submit" class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors">
<i class="fas fa-download mr-2"></i>
Run Update Now
</button>
<a href="/" class="block w-full text-center px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-times mr-2"></i>
Cancel
</a>
</form>
</div>
<div class="text-center mt-6">
<p class="text-gray-500 text-xs">© <?= date('Y') ?> Domain Monitor</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Install Domain Monitor</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#4A90E2', dark: '#357ABD' }
}
}
}
}
</script>
<style>
body { background-color: #f8f9fa; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-2xl w-full">
<!-- Installer Card -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-lg mb-4">
<i class="fas fa-globe text-white text-3xl"></i>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">Domain Monitor Installer</h1>
<p class="text-gray-600">Welcome! Let's set up your monitoring system</p>
</div>
<!-- Installation Steps -->
<div class="bg-gray-50 rounded-lg border border-gray-200 p-6 mb-6">
<h2 class="text-sm font-semibold text-gray-700 uppercase tracking-wider mb-4">Installation Steps</h2>
<div class="space-y-3">
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center text-sm font-semibold">1</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">Database Setup</h3>
<p class="text-sm text-gray-600">Create tables and structure</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center text-sm font-semibold">2</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">Admin Account</h3>
<p class="text-sm text-gray-600">Set your credentials below</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center text-sm font-semibold">3</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-gray-900">Start Monitoring</h3>
<p class="text-sm text-gray-600">Begin tracking your domains</p>
</div>
</div>
</div>
</div>
<!-- Error Alert -->
<?php if (isset($_SESSION['error'])): ?>
<div class="mb-6 bg-red-50 border border-red-200 p-3 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-500 mr-2"></i>
<span class="text-sm text-red-700"><?= htmlspecialchars($_SESSION['error']) ?></span>
</div>
</div>
<?php unset($_SESSION['error']); endif; ?>
<!-- Installation Form -->
<form method="POST" action="/install/run" class="space-y-5">
<div class="border-t border-gray-200 pt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Administrator Account</h3>
<div class="space-y-4">
<div>
<label for="admin_email" class="block text-sm font-medium text-gray-700 mb-2">
Email Address <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-envelope text-gray-400 text-sm"></i>
</div>
<input type="email" id="admin_email" name="admin_email" required
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
placeholder="admin@example.com">
</div>
</div>
<div>
<label for="admin_password" class="block text-sm font-medium text-gray-700 mb-2">
Password <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400 text-sm"></i>
</div>
<input type="password" id="admin_password" name="admin_password" required minlength="8"
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
placeholder="Enter secure password">
<button type="button" onclick="togglePassword()"
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i class="fas fa-eye text-sm" id="toggleIcon"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
</div>
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mt-4">
<p class="text-xs text-blue-800">
<i class="fas fa-info-circle mr-1"></i>
<strong>Note:</strong> These credentials will be used to access the admin panel. Save them securely!
</p>
</div>
</div>
<button type="submit" class="w-full bg-primary hover:bg-primary-dark text-white py-2.5 rounded-lg font-medium transition-colors">
<i class="fas fa-rocket mr-2"></i>
Start Installation
</button>
</form>
</div>
<!-- Footer -->
<div class="text-center mt-6">
<p class="text-gray-500 text-xs">© <?= date('Y') ?> Domain Monitor</p>
</div>
</div>
<script>
function togglePassword() {
const input = document.getElementById('admin_password');
const icon = document.getElementById('toggleIcon');
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('fa-eye', 'fa-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('fa-eye-slash', 'fa-eye');
}
}
</script>
</body>
</html>

View File

@@ -4,61 +4,30 @@
* Contains: HTML structure, meta tags, CSS/JS includes, global stats
*/
// Fetch notifications for top nav (available on all pages)
if (isset($_SESSION['user_id'])) {
$notificationData = \App\Helpers\LayoutHelper::getNotifications($_SESSION['user_id']);
$recentNotifications = $notificationData['items'];
$unreadNotifications = $notificationData['unread_count'];
} else {
$recentNotifications = [];
$unreadNotifications = 0;
}
// Fetch global stats for sidebar (available on all pages)
if (!isset($globalStats)) {
try {
$pdo = \Core\Database::getConnection();
// Get total domains
$totalStmt = $pdo->query("SELECT COUNT(*) as count FROM domains");
$totalResult = $totalStmt->fetch(\PDO::FETCH_ASSOC);
$total = $totalResult['count'] ?? 0;
// Get active domains
$activeStmt = $pdo->query("SELECT COUNT(*) as count FROM domains WHERE is_active = 1");
$activeResult = $activeStmt->fetch(\PDO::FETCH_ASSOC);
$active = $activeResult['count'] ?? 0;
// Get expiring soon - use the first notification threshold from settings
$settingModel = new \App\Models\Setting();
$notificationDays = $settingModel->getNotificationDays();
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30; // Use the largest notification day
$expiringSoonStmt = $pdo->prepare("SELECT COUNT(*) as count FROM domains WHERE is_active = 1 AND expiration_date IS NOT NULL AND expiration_date <= DATE_ADD(NOW(), INTERVAL ? DAY) AND expiration_date >= NOW()");
$expiringSoonStmt->execute([$expiringThreshold]);
$expiringSoonResult = $expiringSoonStmt->fetch(\PDO::FETCH_ASSOC);
$expiringSoon = $expiringSoonResult['count'] ?? 0;
$globalStats = [
'total' => $total,
'active' => $active,
'expiring_soon' => $expiringSoon,
'expiring_threshold' => $expiringThreshold
];
} catch (\Exception $e) {
$globalStats = [
'total' => 0,
'active' => 0,
'expiring_soon' => 0,
'expiring_threshold' => 30
];
}
$globalStats = \App\Helpers\LayoutHelper::getGlobalStats();
}
// Get application settings from database
if (!isset($appName)) {
try {
$settingModel = new \App\Models\Setting();
$appSettings = $settingModel->getAppSettings();
$appName = htmlspecialchars($appSettings['app_name']);
$appTimezone = $appSettings['app_timezone'];
// Set PHP timezone
date_default_timezone_set($appTimezone);
} catch (\Exception $e) {
$appName = 'Domain Monitor';
date_default_timezone_set('UTC');
}
$appSettings = \App\Helpers\LayoutHelper::getAppSettings();
$appName = $appSettings['app_name'];
$appTimezone = $appSettings['app_timezone'];
$appVersion = $appSettings['app_version'];
// Set PHP timezone
date_default_timezone_set($appTimezone);
}
?>
<!DOCTYPE html>
@@ -179,16 +148,44 @@ if (!isset($appName)) {
// Toggle user dropdown
function toggleDropdown() {
document.getElementById('userDropdown').classList.toggle('show');
const dropdown = document.getElementById('userDropdown');
const notifDropdown = document.getElementById('notificationsDropdown');
// Close notifications dropdown if open
if (notifDropdown && notifDropdown.classList.contains('show')) {
notifDropdown.classList.remove('show');
}
dropdown.classList.toggle('show');
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const dropdown = document.getElementById('userDropdown');
const isClickInside = event.target.closest('[onclick="toggleDropdown()"]') || event.target.closest('#userDropdown');
// Toggle notifications dropdown
function toggleNotifications() {
const dropdown = document.getElementById('notificationsDropdown');
const userDropdown = document.getElementById('userDropdown');
if (!isClickInside && dropdown && dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
// Close user dropdown if open
if (userDropdown && userDropdown.classList.contains('show')) {
userDropdown.classList.remove('show');
}
dropdown.classList.toggle('show');
}
// Close dropdowns when clicking outside
document.addEventListener('click', function(event) {
const userDropdown = document.getElementById('userDropdown');
const notifDropdown = document.getElementById('notificationsDropdown');
const isUserDropdownClick = event.target.closest('[onclick="toggleDropdown()"]') || event.target.closest('#userDropdown');
const isNotifDropdownClick = event.target.closest('[onclick="toggleNotifications()"]') || event.target.closest('#notificationsDropdown');
if (!isUserDropdownClick && userDropdown && userDropdown.classList.contains('show')) {
userDropdown.classList.remove('show');
}
if (!isNotifDropdownClick && notifDropdown && notifDropdown.classList.contains('show')) {
notifDropdown.classList.remove('show');
}
});

View File

@@ -33,6 +33,9 @@
<a href="/tld-registry" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/tld-registry') !== false ? 'bg-primary text-white' : '' ?>">
<i class="fas fa-database text-xs mr-3 w-4"></i>
<span class="text-sm">TLD Registry</span>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] !== 'admin'): ?>
<span class="ml-auto text-xs bg-gray-700 px-1.5 py-0.5 rounded text-gray-400">View</span>
<?php endif; ?>
</a>
</div>
@@ -47,7 +50,8 @@
</div>
</div>
<!-- System Section -->
<!-- System Section (Admin Only) -->
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="mt-4 pt-3 border-t border-gray-800">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">System</p>
<div class="space-y-0.5">
@@ -55,8 +59,13 @@
<i class="fas fa-cog text-xs mr-3 w-4"></i>
<span class="text-sm">Settings</span>
</a>
<a href="/users" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/users') !== false ? 'bg-primary text-white' : '' ?>">
<i class="fas fa-users text-xs mr-3 w-4"></i>
<span class="text-sm">Users</span>
</a>
</div>
</div>
<?php endif; ?>
</nav>
<!-- Quick Stats Cards - Pinned to Bottom -->
@@ -105,7 +114,7 @@
<div class="px-4 py-3 border-t border-gray-800">
<div class="text-center">
<p class="text-xs text-gray-500">© <?= date('Y') ?> Domain Monitor</p>
<p class="text-xs text-gray-600 mt-0.5">v1.0.0</p>
<p class="text-xs text-gray-600 mt-0.5">v<?= $appVersion ?></p>
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
<!-- Top Navigation Bar -->
<!-- Notification data ($recentNotifications, $unreadNotifications) loaded in base.php -->
<nav class="bg-white border-b border-gray-200 fixed top-0 left-0 md:left-64 right-0 z-20">
<div class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
@@ -50,25 +51,72 @@
<!-- Right: Actions & User -->
<div class="flex items-center space-x-2">
<!-- Quick Add Domain -->
<a href="/domains/create" title="Add Domain" class="flex items-center justify-center w-9 h-9 bg-primary hover:bg-primary-dark text-white rounded-lg transition-colors duration-150">
<a href="/domains/create" title="Add Domain" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
<i class="fas fa-plus"></i>
</a>
<!-- Notifications -->
<button title="Notifications" class="relative flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
<i class="fas fa-bell"></i>
<?php if (($globalStats['expiring_soon'] ?? 0) > 0): ?>
<span class="absolute top-1 right-1 flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
<?php endif; ?>
</button>
<!-- Settings -->
<button title="Settings" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
<i class="fas fa-cog"></i>
</button>
<div class="relative">
<button onclick="toggleNotifications()" title="Notifications" class="relative flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
<i class="fas fa-bell"></i>
<?php if ($unreadNotifications > 0): ?>
<span class="absolute top-1 right-1 flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-orange-500"></span>
</span>
<?php endif; ?>
</button>
<!-- Notifications Dropdown -->
<div id="notificationsDropdown" class="dropdown-menu absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-xl border border-gray-200 max-h-[32rem] overflow-hidden">
<!-- Header -->
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900">Notifications</h3>
<?php if ($unreadNotifications > 0): ?>
<span class="px-2 py-0.5 bg-orange-100 text-orange-700 text-xs font-semibold rounded"><?= $unreadNotifications ?> new</span>
<?php endif; ?>
</div>
</div>
<!-- Notifications List (Scrollable) -->
<div class="max-h-96 overflow-y-auto">
<?php if (!empty($recentNotifications)): ?>
<?php foreach ($recentNotifications as $notif): ?>
<div class="px-4 py-3 hover:bg-gray-50 border-b border-gray-100 bg-blue-50 transition-colors cursor-pointer">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-<?= $notif['color'] ?>-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-<?= $notif['icon'] ?> text-<?= $notif['color'] ?>-600 text-sm"></i>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($notif['title']) ?></p>
<span class="w-2 h-2 bg-blue-500 rounded-full"></span>
</div>
<p class="text-xs text-gray-600 mt-0.5"><?= htmlspecialchars($notif['message']) ?></p>
<p class="text-xs text-gray-400 mt-1"><?= $notif['time_ago'] ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="px-4 py-8 text-center">
<i class="fas fa-bell-slash text-gray-300 text-3xl mb-2"></i>
<p class="text-sm text-gray-600">No new notifications</p>
<p class="text-xs text-gray-400 mt-0.5">You're all caught up!</p>
</div>
<?php endif; ?>
</div>
<!-- Footer - View All Button -->
<div class="px-4 py-3 border-t border-gray-200 bg-gray-50">
<a href="/notifications" class="block text-center text-sm font-medium text-primary hover:text-primary-dark">
View All Notifications
<i class="fas fa-arrow-right ml-1 text-xs"></i>
</a>
</div>
</div>
</div>
<!-- Divider -->
<div class="hidden md:block h-8 w-px bg-gray-300"></div>
@@ -81,7 +129,9 @@
</div>
<div class="hidden lg:block text-left">
<p class="text-sm font-medium text-gray-700"><?= htmlspecialchars($_SESSION['username'] ?? 'User') ?></p>
<p class="text-xs text-gray-500">Administrator</p>
<p class="text-xs text-gray-500">
<?= ucfirst($_SESSION['role'] ?? 'user') ?>
</p>
</div>
<i class="fas fa-chevron-down text-gray-400 text-xs hidden md:block"></i>
</button>
@@ -90,32 +140,38 @@
<div id="userDropdown" class="dropdown-menu absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 border border-gray-200">
<div class="px-4 py-3 border-b border-gray-200">
<p class="text-sm font-medium text-gray-900"><?= htmlspecialchars($_SESSION['full_name'] ?? $_SESSION['username'] ?? 'User') ?></p>
<p class="text-xs text-gray-500 mt-1"><?= htmlspecialchars($_SESSION['email'] ?? 'admin@example.com') ?></p>
<p class="text-xs text-gray-500 mt-1"><?= htmlspecialchars($_SESSION['email'] ?? 'user@example.com') ?></p>
<span class="inline-block mt-2 px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded">
<i class="fas fa-circle text-xs mr-1"></i>Online
</span>
</div>
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<a href="/profile#profile" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<i class="fas fa-user-circle w-5 text-gray-400 mr-3"></i>
My Profile
</a>
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<a href="/profile#security" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<i class="fas fa-cog w-5 text-gray-400 mr-3"></i>
Account Settings
</a>
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<a href="/notifications" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<i class="fas fa-bell w-5 text-gray-400 mr-3"></i>
Notifications
<?php if ($unreadNotifications > 0): ?>
<span class="ml-auto px-2 py-0.5 bg-orange-500 text-white text-xs font-bold rounded-full">
<?= $unreadNotifications ?>
</span>
<?php endif; ?>
</a>
<div class="border-t border-gray-200 my-1"></div>
<a href="#" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<i class="fas fa-question-circle w-5 text-gray-400 mr-3"></i>
<a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
<i class="fab fa-github w-5 text-gray-400 mr-3"></i>
Help & Support
<i class="fas fa-external-link-alt ml-auto text-xs text-gray-400"></i>
</a>
<div class="border-t border-gray-200 my-1"></div>
@@ -130,4 +186,3 @@
</div>
</div>
</nav>

View File

@@ -0,0 +1,287 @@
<?php
$title = 'Notifications';
$pageTitle = 'Notifications';
$pageDescription = 'View and manage your notifications';
$pageIcon = 'fas fa-bell';
ob_start();
// Data is passed from the controller
$filterType = $filters['type'] ?? '';
$filterStatus = $filters['status'] ?? '';
$filterDateRange = $filters['date_range'] ?? '';
$page = $pagination['current_page'];
$totalPages = $pagination['total_pages'];
$perPage = $pagination['per_page'];
$totalNotifications = $pagination['total'];
$offset = $pagination['showing_from'] - 1;
?>
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<!-- Placeholder for future bulk selection actions -->
</div>
<div class="flex gap-2">
<button onclick="markAllAsRead()" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-check-double mr-2"></i>
Mark All Read
</button>
<button onclick="clearAll()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash-alt mr-2"></i>
Clear All
</button>
</div>
</div>
<!-- Filters & Search -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<form method="GET" action="/notifications" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<!-- Status Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Status</label>
<select name="status" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">All Notifications</option>
<option value="unread" <?= $filterStatus === 'unread' ? 'selected' : '' ?>>Unread Only</option>
<option value="read" <?= $filterStatus === 'read' ? 'selected' : '' ?>>Read Only</option>
</select>
</div>
<!-- Type Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Type</label>
<select name="type" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">All Types</option>
<optgroup label="Domain">
<option value="domain_expiring" <?= $filterType === 'domain_expiring' ? 'selected' : '' ?>>Domain Expiring</option>
<option value="domain_expired" <?= $filterType === 'domain_expired' ? 'selected' : '' ?>>Domain Expired</option>
<option value="domain_updated" <?= $filterType === 'domain_updated' ? 'selected' : '' ?>>Domain Updated</option>
<option value="whois_failed" <?= $filterType === 'whois_failed' ? 'selected' : '' ?>>WHOIS Failed</option>
</optgroup>
<optgroup label="System">
<option value="session_new" <?= $filterType === 'session_new' ? 'selected' : '' ?>>New Login</option>
<option value="system_welcome" <?= $filterType === 'system_welcome' ? 'selected' : '' ?>>Welcome</option>
<option value="system_upgrade" <?= $filterType === 'system_upgrade' ? 'selected' : '' ?>>System Upgrade</option>
</optgroup>
</select>
</div>
<!-- Date Range -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Date Range</label>
<select name="date_range" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">All Time</option>
<option value="today" <?= $filterDateRange === 'today' ? 'selected' : '' ?>>Today</option>
<option value="week" <?= $filterDateRange === 'week' ? 'selected' : '' ?>>This Week</option>
<option value="month" <?= $filterDateRange === 'month' ? 'selected' : '' ?>>This Month</option>
</select>
</div>
<!-- Apply/Reset Buttons -->
<div class="flex items-end space-x-2">
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-filter mr-2"></i>
Apply Filters
</button>
<a href="/notifications" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
</form>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
Showing <span class="font-semibold text-gray-900"><?= $offset + 1 ?></span> to
<span class="font-semibold text-gray-900"><?= min($offset + $perPage, $totalNotifications) ?></span> of
<span class="font-semibold text-gray-900"><?= $totalNotifications ?></span> notification(s)
<?php if ($unreadCount > 0): ?>
<span class="text-gray-400">•</span>
<span class="font-semibold text-blue-600"><?= $unreadCount ?></span> unread
<?php endif; ?>
</div>
<form method="GET" action="/notifications" class="flex items-center gap-2">
<!-- Preserve current filters -->
<input type="hidden" name="status" value="<?= htmlspecialchars($filterStatus) ?>">
<input type="hidden" name="type" value="<?= htmlspecialchars($filterType) ?>">
<label for="per_page" class="text-sm text-gray-600">Show:</label>
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="10" <?= $perPage == 10 ? 'selected' : '' ?>>10</option>
<option value="25" <?= $perPage == 25 ? 'selected' : '' ?>>25</option>
<option value="50" <?= $perPage == 50 ? 'selected' : '' ?>>50</option>
<option value="100" <?= $perPage == 100 ? 'selected' : '' ?>>100</option>
</select>
</form>
</div>
<!-- Notifications List -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (!empty($notifications)): ?>
<div class="divide-y divide-gray-100">
<?php foreach ($notifications as $notification): ?>
<?php
$bgClass = $notification['is_read'] ? '' : 'bg-blue-50';
$iconBgClass = "bg-{$notification['color']}-100";
$iconTextClass = "text-{$notification['color']}-600";
?>
<div class="px-4 py-3 hover:bg-gray-50 transition-colors <?= $bgClass ?>">
<div class="flex items-center gap-3">
<!-- Icon -->
<div class="w-8 h-8 <?= $iconBgClass ?> rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-<?= $notification['icon'] ?> <?= $iconTextClass ?> text-xs"></i>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<h3 class="text-sm font-medium text-gray-900"><?= htmlspecialchars($notification['title']) ?></h3>
<?php if (!$notification['is_read']): ?>
<span class="flex h-1.5 w-1.5">
<span class="animate-ping absolute inline-flex h-1.5 w-1.5 rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-blue-500"></span>
</span>
<?php endif; ?>
<span class="text-xs text-gray-400 ml-auto">
<i class="fas fa-clock mr-1"></i>
<?= $notification['time_ago'] ?>
</span>
</div>
<p class="text-xs text-gray-600 mt-0.5"><?= htmlspecialchars($notification['message']) ?></p>
</div>
<!-- Actions -->
<div class="flex items-center gap-1 ml-2">
<?php if (!$notification['is_read']): ?>
<a href="/notifications/<?= $notification['id'] ?>/mark-read" class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-green-600 hover:bg-green-50 rounded transition-colors" title="Mark as read">
<i class="fas fa-check text-xs"></i>
</a>
<?php endif; ?>
<a href="/notifications/<?= $notification['id'] ?>/delete" onclick="return confirm('Delete this notification?')" class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors" title="Delete">
<i class="fas fa-times text-xs"></i>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<!-- Empty State -->
<div class="p-12 text-center">
<i class="fas fa-bell-slash text-gray-300 text-4xl mb-3"></i>
<p class="text-sm text-gray-600">No notifications found</p>
<p class="text-xs text-gray-400 mt-1">Try adjusting your filters</p>
</div>
<?php endif; ?>
</div>
<!-- Pagination Controls -->
<?php if ($totalPages > 1): ?>
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Page Info -->
<div class="text-sm text-gray-600">
Page <span class="font-semibold text-gray-900"><?= $page ?></span> of
<span class="font-semibold text-gray-900"><?= $totalPages ?></span>
</div>
<!-- Pagination Buttons -->
<div class="flex items-center gap-1">
<?php
// Helper function to build pagination URL
function paginationUrl($page, $status, $type) {
$params = $_GET;
$params['page'] = $page;
if ($status) $params['status'] = $status;
if ($type) $params['type'] = $type;
return '/notifications?' . http_build_query($params);
}
?>
<!-- First Page -->
<?php if ($page > 1): ?>
<a href="<?= paginationUrl(1, $filterStatus, $filterType) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-left"></i>
</a>
<?php endif; ?>
<!-- Previous Page -->
<?php if ($page > 1): ?>
<a href="<?= paginationUrl($page - 1, $filterStatus, $filterType) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-left"></i> Previous
</a>
<?php endif; ?>
<!-- Page Numbers -->
<?php
$range = 2; // Show 2 pages on each side of current page
$start = max(1, $page - $range);
$end = min($totalPages, $page + $range);
// Show first page + ellipsis if needed
if ($start > 1) {
echo '<a href="' . paginationUrl(1, $filterStatus, $filterType) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">1</a>';
if ($start > 2) {
echo '<span class="px-2 text-gray-500">...</span>';
}
}
// Page numbers
for ($i = $start; $i <= $end; $i++) {
if ($i == $page) {
echo '<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">' . $i . '</span>';
} else {
echo '<a href="' . paginationUrl($i, $filterStatus, $filterType) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $i . '</a>';
}
}
// Show last page + ellipsis if needed
if ($end < $totalPages) {
if ($end < $totalPages - 1) {
echo '<span class="px-2 text-gray-500">...</span>';
}
echo '<a href="' . paginationUrl($totalPages, $filterStatus, $filterType) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $totalPages . '</a>';
}
?>
<!-- Next Page -->
<?php if ($page < $totalPages): ?>
<a href="<?= paginationUrl($page + 1, $filterStatus, $filterType) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
Next <i class="fas fa-angle-right"></i>
</a>
<?php endif; ?>
<!-- Last Page -->
<?php if ($page < $totalPages): ?>
<a href="<?= paginationUrl($totalPages, $filterStatus, $filterType) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-right"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<script>
function markAllAsRead() {
if (confirm('Mark all notifications as read?')) {
window.location.href = '/notifications/mark-all-read';
}
}
function clearAll() {
if (confirm('Clear all notifications? This action cannot be undone.')) {
window.location.href = '/notifications/clear-all';
}
}
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';
?>

481
app/Views/profile/index.php Normal file
View File

@@ -0,0 +1,481 @@
<?php
$title = 'My Profile';
$pageTitle = 'My Profile';
$pageDescription = 'Manage your account settings and preferences';
$pageIcon = 'fas fa-user-circle';
ob_start();
?>
<!-- Main Profile Layout -->
<div class="grid grid-cols-12 gap-6">
<!-- Sidebar Navigation -->
<div class="col-span-12 lg:col-span-3">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden sticky top-6">
<!-- User Info Section -->
<div class="p-6 border-b border-gray-200 bg-gray-50">
<div class="flex flex-col items-center text-center">
<div class="w-20 h-20 rounded-full bg-primary flex items-center justify-center text-white text-2xl font-bold">
<?= strtoupper(substr($user['username'] ?? 'U', 0, 1)) ?>
</div>
<h3 class="mt-4 text-base font-semibold text-gray-900"><?= htmlspecialchars($user['full_name'] ?? $user['username']) ?></h3>
<p class="text-sm text-gray-500 mt-1">@<?= htmlspecialchars($user['username'] ?? '') ?></p>
<!-- Role Badge -->
<span class="inline-flex items-center mt-3 px-2.5 py-1 bg-<?= $user['role'] === 'admin' ? 'indigo' : 'blue' ?>-100 text-<?= $user['role'] === 'admin' ? 'indigo' : 'blue' ?>-800 text-xs font-semibold rounded">
<i class="fas fa-<?= $user['role'] === 'admin' ? 'crown' : 'user' ?> mr-1.5"></i>
<?= ucfirst($user['role'] ?? 'user') ?>
</span>
<!-- Stats -->
<div class="grid grid-cols-2 gap-3 mt-4 w-full">
<div class="bg-white rounded-lg p-2 border border-gray-200">
<div class="text-xs text-gray-500">Member Since</div>
<div class="text-xs font-semibold text-gray-900 mt-0.5">
<?= date('M Y', strtotime($user['created_at'] ?? 'now')) ?>
</div>
</div>
<div class="bg-white rounded-lg p-2 border border-gray-200">
<div class="text-xs text-gray-500">Status</div>
<div class="text-xs font-semibold text-green-600 mt-0.5">
<i class="fas fa-circle text-xs"></i> Active
</div>
</div>
</div>
</div>
</div>
<!-- Navigation Links -->
<nav class="p-3">
<button onclick="showSection('profile')" id="nav-profile" class="nav-item active w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-user-circle w-5 mr-3 text-sm"></i>
<span>Profile Information</span>
</button>
<button onclick="showSection('security')" id="nav-security" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-shield-alt w-5 mr-3 text-sm"></i>
<span>Security</span>
</button>
<button onclick="showSection('sessions')" id="nav-sessions" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors mb-1">
<i class="fas fa-laptop w-5 mr-3 text-sm"></i>
<span>Active Sessions</span>
</button>
<?php if ($user['role'] !== 'admin'): ?>
<hr class="my-3 border-gray-200">
<button onclick="showSection('danger')" id="nav-danger" class="nav-item w-full flex items-center px-4 py-2.5 text-sm font-medium rounded-lg transition-colors text-red-600 hover:bg-red-50">
<i class="fas fa-exclamation-triangle w-5 mr-3 text-sm"></i>
<span>Danger Zone</span>
</button>
<?php endif; ?>
</nav>
</div>
</div>
<!-- Main Content Area -->
<div class="col-span-12 lg:col-span-9">
<!-- Profile Information Section -->
<div id="section-profile" class="content-section">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900">Profile Information</h3>
<p class="text-sm text-gray-600 mt-1">Update your personal details and account information</p>
</div>
<form method="POST" action="/profile/update" class="p-6">
<div class="space-y-5">
<!-- Full Name -->
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input type="text" id="full_name" name="full_name"
value="<?= htmlspecialchars($user['full_name'] ?? '') ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input type="email" id="email" name="email"
value="<?= htmlspecialchars($user['email'] ?? '') ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<?php if (!empty($user['email_verified'])): ?>
<p class="text-xs text-green-600 mt-1.5">
<i class="fas fa-check-circle mr-1"></i>
Email verified
</p>
<?php else: ?>
<div class="mt-3 bg-amber-50 border border-amber-200 rounded-lg p-3">
<div class="flex items-start justify-between">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-amber-600 mt-0.5 mr-2"></i>
<div>
<p class="text-xs font-semibold text-amber-900">Email Not Verified</p>
<p class="text-xs text-amber-700 mt-0.5">Verify your email to unlock all features</p>
</div>
</div>
<a href="/profile/resend-verification" class="ml-3 inline-flex items-center px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-xs rounded-lg transition-colors font-medium whitespace-nowrap">
<i class="fas fa-paper-plane mr-1.5"></i>
Resend
</a>
</div>
</div>
<?php endif; ?>
</div>
<!-- Username (Read-only) -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input type="text" id="username" name="username"
value="<?= htmlspecialchars($user['username'] ?? '') ?>"
readonly
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed">
<p class="text-xs text-gray-500 mt-1">Username cannot be changed</p>
</div>
<!-- Account Details Grid -->
<div class="pt-4 border-t border-gray-200">
<h4 class="text-sm font-semibold text-gray-700 mb-3">Account Information</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<label class="block text-xs font-medium text-gray-500 mb-1">Member Since</label>
<p class="text-sm font-semibold text-gray-900">
<?= date('F j, Y', strtotime($user['created_at'] ?? 'now')) ?>
</p>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<label class="block text-xs font-medium text-gray-500 mb-1">Last Login</label>
<p class="text-sm font-semibold text-gray-900">
<?= $user['last_login'] ? date('M j, Y g:i A', strtotime($user['last_login'])) : 'Never' ?>
</p>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end pt-6 mt-6 border-t border-gray-200 space-x-2">
<button type="button" onclick="location.reload()" class="px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
Cancel
</button>
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-save mr-2"></i>
Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- Security Section -->
<div id="section-security" class="content-section hidden">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900">Security Settings</h3>
<p class="text-sm text-gray-600 mt-1">Manage your password and security preferences</p>
</div>
<form method="POST" action="/profile/change-password" class="p-6">
<div class="space-y-4">
<!-- Current Password -->
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700 mb-2">
Current Password
</label>
<input type="password" id="current_password" name="current_password" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
placeholder="Enter your current password">
</div>
<!-- New Password -->
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input type="password" id="new_password" name="new_password" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
placeholder="Enter a strong password">
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
</div>
<!-- Confirm New Password -->
<div>
<label for="new_password_confirm" class="block text-sm font-medium text-gray-700 mb-2">
Confirm New Password
</label>
<input type="password" id="new_password_confirm" name="new_password_confirm" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
placeholder="Re-enter your new password">
</div>
<!-- Password Tips -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p class="text-xs text-gray-600">
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
Use at least 8 characters with a mix of letters, numbers, and symbols for better security.
</p>
</div>
</div>
<div class="flex items-center justify-end pt-6 mt-6 border-t border-gray-200">
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-key mr-2"></i>
Update Password
</button>
</div>
</form>
</div>
</div>
<!-- Active Sessions Section -->
<div id="section-sessions" class="content-section hidden">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900">Active Sessions</h3>
<p class="text-sm text-gray-600 mt-1">Manage devices and sessions where you're logged in (<?= count($sessions ?? []) ?> active)</p>
</div>
<?php if (count($sessions ?? []) > 1): ?>
<form method="POST" action="/profile/logout-other-sessions" onsubmit="return confirm('Logout all other sessions?')" class="inline">
<button type="submit" class="inline-flex items-center px-3 py-2 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-sign-out-alt mr-1.5"></i>
Logout Others
</button>
</form>
<?php endif; ?>
</div>
</div>
<div class="p-6">
<?php if (!empty($sessions)): ?>
<div class="space-y-3">
<?php foreach ($sessions as $session): ?>
<?php
// Display data prepared by SessionHelper in controller
$deviceIcon = $session['deviceIcon'];
$browserInfo = $session['browserInfo'];
$timeAgo = $session['timeAgo'];
$sessionAge = $session['sessionAge'];
$isCurrent = $session['is_current'] ?? false;
$bgClass = $isCurrent ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200';
?>
<div class="flex items-start justify-between p-4 <?= $bgClass ?> border rounded-lg">
<div class="flex items-start space-x-3 flex-1">
<!-- Device Icon -->
<div class="w-10 h-10 bg-<?= $isCurrent ? 'green' : 'gray' ?>-100 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas <?= $deviceIcon ?> text-<?= $isCurrent ? 'green' : 'gray' ?>-600"></i>
</div>
<div class="flex-1 min-w-0">
<!-- Header -->
<div class="flex items-center flex-wrap gap-2">
<?php if (!empty($session['country_code']) && $session['country_code'] !== 'xx'): ?>
<span class="fi fi-<?= strtolower($session['country_code']) ?> text-base"></span>
<?php endif; ?>
<h4 class="text-sm font-semibold text-gray-900">
<?= htmlspecialchars($session['city'] ?? 'Unknown') ?>, <?= htmlspecialchars($session['country'] ?? 'Unknown') ?>
</h4>
<?php if ($isCurrent): ?>
<span class="px-2 py-0.5 bg-green-500 text-white text-xs font-semibold rounded">
Current
</span>
<?php endif; ?>
<?php if (!empty($session['has_remember_token'])): ?>
<span class="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs font-semibold rounded" title="Remember me enabled">
<i class="fas fa-cookie-bite"></i>
</span>
<?php endif; ?>
</div>
<!-- Browser & OS -->
<p class="text-xs text-gray-600 mt-1">
<i class="fas fa-globe mr-1"></i>
<?= htmlspecialchars($browserInfo) ?>
<?php if (!empty($session['user_agent'])): ?>
- <?= htmlspecialchars(substr($session['user_agent'], 0, 60)) ?><?= strlen($session['user_agent']) > 60 ? '...' : '' ?>
<?php endif; ?>
</p>
<!-- IP & ISP -->
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-500 mt-1">
<span>
<i class="fas fa-map-marker-alt mr-1"></i>
<?= htmlspecialchars($session['ip_address']) ?>
</span>
<?php if (!empty($session['isp'])): ?>
<span>
<i class="fas fa-network-wired mr-1"></i>
<?= htmlspecialchars($session['isp']) ?>
</span>
<?php endif; ?>
</div>
<!-- Session Age & Last Activity -->
<div class="flex flex-wrap items-center gap-3 text-xs text-gray-400 mt-1">
<span title="Session started: <?= date('M j, Y H:i', strtotime($session['created_at'])) ?>">
<i class="fas fa-hourglass-start mr-1"></i>
<?= $sessionAge ?>
</span>
<span>
<i class="fas fa-clock mr-1"></i>
Active <?= $timeAgo ?>
</span>
</div>
</div>
</div>
<!-- Delete Button (only for non-current sessions) -->
<?php if (!$isCurrent): ?>
<form method="POST" action="/profile/logout-session/<?= htmlspecialchars($session['id']) ?>" onsubmit="return confirm('Terminate this session?\n\nThat device will be logged out immediately.')" class="ml-3">
<button type="submit" class="flex items-center justify-center w-8 h-8 bg-red-100 text-red-600 rounded-lg hover:bg-red-600 hover:text-white transition-colors" title="Terminate session">
<i class="fas fa-times text-sm"></i>
</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Info Box -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mt-4">
<p class="text-xs text-gray-600">
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
If you see any suspicious sessions or don't recognize a device, logout other sessions immediately and change your password.
</p>
</div>
<?php else: ?>
<div class="text-center py-8">
<i class="fas fa-laptop text-gray-300 text-4xl mb-3"></i>
<p class="text-sm text-gray-600">No active sessions found</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Danger Zone Section -->
<?php if ($user['role'] !== 'admin'): ?>
<div id="section-danger" class="content-section hidden">
<div class="bg-white rounded-lg border border-red-200 overflow-hidden">
<div class="px-6 py-4 border-b border-red-200 bg-red-50">
<h3 class="text-lg font-semibold text-red-900">Danger Zone</h3>
<p class="text-sm text-red-700 mt-1">Irreversible and destructive actions</p>
</div>
<div class="p-6">
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-sm font-bold text-red-900">Delete Account Permanently</h4>
<p class="text-sm text-red-700 mt-2">
Once you delete your account, there is no going back. This will permanently delete all your profile information and account settings.
</p>
<p class="text-xs text-red-800 font-semibold mt-3 bg-red-100 inline-block px-2 py-1 rounded">
This action cannot be undone
</p>
</div>
<button onclick="confirmDelete()" class="ml-4 inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium whitespace-nowrap">
<i class="fas fa-trash-alt mr-2"></i>
Delete Account
</button>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<style>
/* Navigation Styles */
.nav-item {
color: #6b7280;
text-align: left;
}
.nav-item:hover {
background-color: #f3f4f6;
color: #1f2937;
}
.nav-item.active {
background-color: #EFF6FF;
color: #4A90E2;
font-weight: 600;
}
/* Content Section Animations */
.content-section {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<script>
function showSection(section) {
// Hide all sections
document.querySelectorAll('.content-section').forEach(el => {
el.classList.add('hidden');
});
// Remove active class from all nav items
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.remove('active');
});
// Show selected section
document.getElementById('section-' + section).classList.remove('hidden');
// Add active class to selected nav item
document.getElementById('nav-' + section).classList.add('active');
// Update URL hash
window.location.hash = section;
// Scroll to top smoothly
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// On page load, check URL hash and show that section
document.addEventListener('DOMContentLoaded', function() {
const hash = window.location.hash.substring(1); // Remove #
const validSections = ['profile', 'security', 'sessions'<?php if ($user['role'] !== 'admin'): ?>, 'danger'<?php endif; ?>];
if (hash && validSections.includes(hash)) {
showSection(hash);
} else {
// Default to profile section
showSection('profile');
}
});
function confirmDelete() {
if (confirm('Are you absolutely sure you want to delete your account?\n\nThis action is PERMANENT and cannot be undone!')) {
if (confirm('FINAL WARNING: This will permanently delete all your data.\n\nClick OK to proceed.')) {
window.location.href = '/profile/delete';
}
}
}
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';
?>

View File

@@ -74,13 +74,9 @@ ob_start();
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($existingDomains as $domain): ?>
<?php
$daysLeft = !empty($domain['expiration_date']) ? floor((strtotime($domain['expiration_date']) - time()) / 86400) : null;
$expiryClass = '';
if ($daysLeft !== null) {
if ($daysLeft < 0) $expiryClass = 'text-red-600 font-semibold';
elseif ($daysLeft <= 30) $expiryClass = 'text-orange-600 font-semibold';
elseif ($daysLeft <= 90) $expiryClass = 'text-yellow-600';
}
// Display data prepared by DomainHelper in controller
$daysLeft = $domain['daysLeft'];
$expiryClass = $domain['expiryClass'];
?>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4">

View File

@@ -118,6 +118,41 @@ foreach ($notificationPresets as $key => $preset) {
</select>
<p class="text-xs text-gray-500 mt-1">Application timezone for dates and times</p>
</div>
<!-- User Registration Settings -->
<div class="border-t border-gray-200 pt-4 mt-6">
<h4 class="text-base font-semibold text-gray-900 mb-4">User Registration</h4>
<div class="space-y-3">
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" id="registration_enabled" name="registration_enabled" value="1"
<?= !empty($settings['registration_enabled']) ? 'checked' : '' ?>
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
</div>
<div class="ml-3">
<label for="registration_enabled" class="text-sm font-medium text-gray-700">
Enable User Registration
</label>
<p class="text-xs text-gray-500 mt-1">Allow new users to create accounts via registration form</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" id="require_email_verification" name="require_email_verification" value="1"
<?= !empty($settings['require_email_verification']) ? 'checked' : '' ?>
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
</div>
<div class="ml-3">
<label for="require_email_verification" class="text-sm font-medium text-gray-700">
Require Email Verification
</label>
<p class="text-xs text-gray-500 mt-1">Users must verify their email address before accessing the system</p>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between pt-6 mt-6 border-t border-gray-200">

View File

@@ -30,6 +30,7 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
<!-- Action Buttons -->
<div class="mb-4">
<div class="flex flex-wrap gap-2 justify-between items-center">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="flex flex-wrap gap-2">
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<input type="hidden" name="import_type" value="complete_workflow">
@@ -45,14 +46,23 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
Check Updates
</button>
</form>
</div>
<div class="flex gap-2">
<a href="/tld-registry/import-logs" class="inline-flex items-center px-4 py-2.5 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-history mr-2"></i>
Import Logs
</a>
</div>
<?php else: ?>
<div>
<p class="text-sm text-gray-600">
<i class="fas fa-info-circle mr-1"></i>
View-only mode. Contact admin to import or modify TLD data.
</p>
</div>
<?php endif; ?>
<div class="flex gap-2">
<!-- Search and filters will stay visible for all users -->
</div>
</div>
</div>
@@ -188,8 +198,8 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
</form>
</div>
<!-- Bulk Actions -->
<?php if (!empty($tlds)): ?>
<!-- Bulk Actions (Admin Only) -->
<?php if (!empty($tlds) && isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
@@ -216,9 +226,11 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-primary focus:ring-primary" onchange="toggleAllCheckboxes(this)">
</th>
<?php endif; ?>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('tld', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
TLD <?= sortIcon('tld', $currentFilters['sort'], $currentFilters['order']) ?>
@@ -250,9 +262,11 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($tlds as $tld): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox" name="tld_ids[]" value="<?= $tld['id'] ?>" class="tld-checkbox rounded border-gray-300 text-primary focus:ring-primary">
</td>
<?php endif; ?>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
@@ -320,12 +334,14 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
<a href="/tld-registry/<?= $tld['id'] ?>" class="text-blue-600 hover:text-blue-800" title="View">
<i class="fas fa-eye"></i>
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="text-green-600 hover:text-green-800" title="Refresh" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt"></i>
</a>
<a href="/tld-registry/<?= $tld['id'] ?>/toggle-active" class="text-orange-600 hover:text-orange-800" title="Toggle Status" onclick="return confirm('Toggle TLD status?')">
<i class="fas fa-power-off"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
@@ -390,12 +406,14 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
</div>
<div class="flex space-x-2 mt-3">
<a href="/tld-registry/<?= $tld['id'] ?>" class="flex-1 px-3 py-1.5 bg-blue-50 text-blue-600 rounded text-center text-sm hover:bg-blue-100 transition-colors">
<a href="/tld-registry/<?= $tld['id'] ?>" class="<?= (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') ? 'flex-1' : 'w-full' ?> px-3 py-1.5 bg-blue-50 text-blue-600 rounded text-center text-sm hover:bg-blue-100 transition-colors">
<i class="fas fa-eye mr-1"></i> View
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="flex-1 px-3 py-1.5 bg-green-50 text-green-600 rounded text-center text-sm hover:bg-green-100 transition-colors" onclick="return confirm('Refresh TLD data?')">
<i class="fas fa-sync-alt mr-1"></i> Refresh
</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>

View File

@@ -19,6 +19,7 @@ ob_start();
</span>
</div>
<div class="flex gap-2 items-center">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="inline-flex items-center justify-center px-3 py-2 bg-green-600 text-white text-xs rounded-lg hover:bg-green-700 transition-colors font-medium min-w-[80px] h-[32px]" onclick="return confirm('Refresh TLD data from IANA?')">
<i class="fas fa-sync-alt mr-1.5"></i>
Refresh
@@ -27,6 +28,7 @@ ob_start();
<i class="fas fa-power-off mr-1.5"></i>
Toggle
</a>
<?php endif; ?>
<a href="/tld-registry" class="inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-gray-700 text-xs rounded-lg hover:bg-gray-50 transition-colors font-medium min-w-[80px] h-[32px]">
<i class="fas fa-arrow-left mr-1.5"></i>
Back
@@ -189,6 +191,7 @@ ob_start();
</h3>
</div>
<div class="p-4 space-y-2">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/tld-registry/<?= $tld['id'] ?>/refresh" class="flex items-center p-3 border border-gray-200 hover:border-green-500 hover:bg-green-50 rounded-lg transition-all duration-200 group" onclick="return confirm('Refresh TLD data from IANA?')">
<div class="w-9 h-9 bg-green-50 group-hover:bg-green-500 rounded-lg flex items-center justify-center group-hover:text-white text-green-600 transition-colors duration-200">
<i class="fas fa-sync-alt text-sm"></i>
@@ -201,6 +204,7 @@ ob_start();
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-orange-700">Toggle Status</span>
</a>
<?php endif; ?>
<?php if ($tld['registry_url']): ?>
<a href="<?= htmlspecialchars($tld['registry_url']) ?>" target="_blank" class="flex items-center p-3 border border-gray-200 hover:border-blue-500 hover:bg-blue-50 rounded-lg transition-all duration-200 group">
<div class="w-9 h-9 bg-blue-50 group-hover:bg-blue-500 rounded-lg flex items-center justify-center group-hover:text-white text-blue-600 transition-colors duration-200">

100
app/Views/users/create.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
$title = 'Create User';
$pageTitle = 'Create User';
$pageDescription = 'Add a new user to the system';
$pageIcon = 'fas fa-user-plus';
ob_start();
?>
<form method="POST" action="/users/store" class="max-w-2xl">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900">User Information</h3>
</div>
<div class="p-6 space-y-4">
<!-- Full Name -->
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 mb-2">
Full Name <span class="text-red-500">*</span>
</label>
<input type="text" id="full_name" name="full_name" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<!-- Username -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
Username <span class="text-red-500">*</span>
</label>
<input type="text" id="username" name="username" required pattern="[a-zA-Z0-9_]+"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<p class="text-xs text-gray-500 mt-1">Letters, numbers, and underscores only</p>
</div>
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email Address <span class="text-red-500">*</span>
</label>
<input type="email" id="email" name="email" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<!-- Role -->
<div>
<label for="role" class="block text-sm font-medium text-gray-700 mb-2">
Role <span class="text-red-500">*</span>
</label>
<select id="role" name="role" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<p class="text-xs text-gray-500 mt-1">Admins have full system access</p>
</div>
<!-- Password -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
Password <span class="text-red-500">*</span>
</label>
<input type="password" id="password" name="password" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<p class="text-xs text-gray-500 mt-1">Minimum 8 characters</p>
</div>
<!-- Confirm Password -->
<div>
<label for="password_confirm" class="block text-sm font-medium text-gray-700 mb-2">
Confirm Password <span class="text-red-500">*</span>
</label>
<input type="password" id="password_confirm" name="password_confirm" required minlength="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p class="text-xs text-blue-800">
<i class="fas fa-info-circle mr-1"></i>
<strong>Note:</strong> Admin-created users are automatically verified and can log in immediately.
</p>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between">
<a href="/users" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
<i class="fas fa-arrow-left mr-1"></i> Cancel
</a>
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-save mr-2"></i>
Create User
</button>
</div>
</div>
</form>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';
?>

130
app/Views/users/edit.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
$title = 'Edit User';
$pageTitle = 'Edit User';
$pageDescription = 'Update user information and permissions';
$pageIcon = 'fas fa-user-edit';
ob_start();
?>
<form method="POST" action="/users/update" class="max-w-2xl">
<input type="hidden" name="id" value="<?= $user['id'] ?>">
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900">User Information</h3>
</div>
<div class="p-6 space-y-4">
<!-- Full Name -->
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 mb-2">
Full Name <span class="text-red-500">*</span>
</label>
<input type="text" id="full_name" name="full_name" required
value="<?= htmlspecialchars($user['full_name'] ?? '') ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<!-- Username (Read-only) -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input type="text" id="username" value="<?= htmlspecialchars($user['username']) ?>" readonly
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed">
<p class="text-xs text-gray-500 mt-1">Username cannot be changed</p>
</div>
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email Address <span class="text-red-500">*</span>
</label>
<input type="email" id="email" name="email" required
value="<?= htmlspecialchars($user['email']) ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
</div>
<!-- Role -->
<div>
<label for="role" class="block text-sm font-medium text-gray-700 mb-2">
Role <span class="text-red-500">*</span>
</label>
<select id="role" name="role" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<option value="user" <?= $user['role'] === 'user' ? 'selected' : '' ?>>User</option>
<option value="admin" <?= $user['role'] === 'admin' ? 'selected' : '' ?>>Admin</option>
</select>
</div>
<!-- Status -->
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" id="is_active" name="is_active" value="1"
<?= $user['is_active'] ? 'checked' : '' ?>
class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary">
</div>
<div class="ml-3">
<label for="is_active" class="text-sm font-medium text-gray-700">
Active
</label>
<p class="text-xs text-gray-500">Inactive users cannot log in</p>
</div>
</div>
<!-- Password (Optional) -->
<div class="border-t border-gray-200 pt-4 mt-4">
<h4 class="text-sm font-semibold text-gray-900 mb-3">Change Password (Optional)</h4>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<input type="password" id="password" name="password" minlength="8"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<p class="text-xs text-gray-500 mt-1">Leave blank to keep current password. Minimum 8 characters if changing.</p>
</div>
</div>
<!-- Account Info -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 mt-4">
<div class="grid grid-cols-2 gap-3 text-xs">
<div>
<span class="text-gray-600">Email Verified:</span>
<span class="font-semibold <?= $user['email_verified'] ? 'text-green-600' : 'text-red-600' ?>">
<?= $user['email_verified'] ? 'Yes' : 'No' ?>
</span>
</div>
<div>
<span class="text-gray-600">Member Since:</span>
<span class="font-semibold text-gray-900">
<?= date('M d, Y', strtotime($user['created_at'])) ?>
</span>
</div>
<div>
<span class="text-gray-600">Last Login:</span>
<span class="font-semibold text-gray-900">
<?= $user['last_login'] ? date('M d, Y H:i', strtotime($user['last_login'])) : 'Never' ?>
</span>
</div>
</div>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between">
<a href="/users" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
<i class="fas fa-arrow-left mr-1"></i> Cancel
</a>
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-save mr-2"></i>
Update User
</button>
</div>
</div>
</form>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';
?>

355
app/Views/users/index.php Normal file
View File

@@ -0,0 +1,355 @@
<?php
$title = 'User Management';
$pageTitle = 'User Management';
$pageDescription = 'Manage system users and permissions';
$pageIcon = 'fas fa-users';
ob_start();
// Helper function to generate sort URL
function sortUrl($column, $currentSort, $currentOrder) {
$newOrder = ($currentSort === $column && $currentOrder === 'asc') ? 'desc' : 'asc';
$params = $_GET;
$params['sort'] = $column;
$params['order'] = $newOrder;
return '/users?' . http_build_query($params);
}
// Helper function for sort icon
function sortIcon($column, $currentSort, $currentOrder) {
if ($currentSort !== $column) {
return '<i class="fas fa-sort text-gray-400 ml-1 text-xs"></i>';
}
$icon = $currentOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down';
return '<i class="fas ' . $icon . ' text-primary ml-1 text-xs"></i>';
}
// Get current filters
$currentFilters = $filters ?? ['search' => '', 'role' => '', 'status' => '', 'sort' => 'username', 'order' => 'asc'];
// Mock pagination for now (will need to be implemented in controller)
$pagination = $pagination ?? [
'current_page' => 1,
'total_pages' => 1,
'per_page' => 25,
'total' => count($users),
'showing_from' => 1,
'showing_to' => count($users)
];
?>
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<!-- Placeholder for future bulk actions -->
</div>
<div class="flex gap-2">
<a href="/users/create" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-user-plus mr-2"></i>
Add User
</a>
</div>
</div>
<!-- Filters & Search -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<form method="GET" action="/users" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<!-- Search -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Search</label>
<div class="relative">
<input type="text" name="search" value="<?= htmlspecialchars($currentFilters['search']) ?>" placeholder="Search users..." class="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs"></i>
</div>
</div>
<!-- Role Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Role</label>
<select name="role" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">All Roles</option>
<option value="admin" <?= $currentFilters['role'] === 'admin' ? 'selected' : '' ?>>Admin</option>
<option value="user" <?= $currentFilters['role'] === 'user' ? 'selected' : '' ?>>User</option>
</select>
</div>
<!-- Status Filter -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Status</label>
<select name="status" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">All Statuses</option>
<option value="active" <?= $currentFilters['status'] === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= $currentFilters['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
<!-- Apply/Reset Buttons -->
<div class="flex items-end space-x-2">
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-filter mr-2"></i>
Apply Filters
</button>
<a href="/users" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
</form>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
Showing <span class="font-semibold text-gray-900"><?= $pagination['showing_from'] ?></span> to
<span class="font-semibold text-gray-900"><?= $pagination['showing_to'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total'] ?></span> user(s)
</div>
<form method="GET" action="/users" class="flex items-center gap-2">
<!-- Preserve current filters -->
<input type="hidden" name="search" value="<?= htmlspecialchars($currentFilters['search']) ?>">
<input type="hidden" name="role" value="<?= htmlspecialchars($currentFilters['role']) ?>">
<input type="hidden" name="status" value="<?= htmlspecialchars($currentFilters['status']) ?>">
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
<label for="per_page" class="text-sm text-gray-600">Show:</label>
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary focus:border-primary">
<option value="10" <?= $pagination['per_page'] == 10 ? 'selected' : '' ?>>10</option>
<option value="25" <?= $pagination['per_page'] == 25 ? 'selected' : '' ?>>25</option>
<option value="50" <?= $pagination['per_page'] == 50 ? 'selected' : '' ?>>50</option>
<option value="100" <?= $pagination['per_page'] == 100 ? 'selected' : '' ?>>100</option>
</select>
</form>
</div>
<!-- Users Table -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (!empty($users)): ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('full_name', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
User <?= sortIcon('full_name', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('username', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Username <?= sortIcon('username', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('role', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Role <?= sortIcon('role', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('is_active', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Status <?= sortIcon('is_active', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('email_verified', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Email Verified <?= sortIcon('email_verified', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
<a href="<?= sortUrl('last_login', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
Last Login <?= sortIcon('last_login', $currentFilters['sort'], $currentFilters['order']) ?>
</a>
</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($users as $user): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<span class="text-primary font-semibold text-sm">
<?= strtoupper(substr($user['username'], 0, 1)) ?>
</span>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($user['full_name'] ?? 'N/A') ?></div>
<div class="text-xs text-gray-500"><?= htmlspecialchars($user['email']) ?></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900"><?= htmlspecialchars($user['username']) ?></div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border
<?= $user['role'] === 'admin' ? 'bg-purple-100 text-purple-700 border-purple-200' : 'bg-blue-100 text-blue-700 border-blue-200' ?>">
<i class="fas fa-<?= $user['role'] === 'admin' ? 'crown' : 'user' ?> mr-1"></i>
<?= ucfirst($user['role']) ?>
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border
<?= $user['is_active'] ? 'bg-green-100 text-green-700 border-green-200' : 'bg-red-100 text-red-700 border-red-200' ?>">
<i class="fas fa-<?= $user['is_active'] ? 'check-circle' : 'times-circle' ?> mr-1"></i>
<?= $user['is_active'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<?php if ($user['email_verified']): ?>
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm text-gray-900">Verified</span>
<?php else: ?>
<i class="fas fa-times-circle text-red-500 mr-2"></i>
<span class="text-sm text-gray-500">Not Verified</span>
<?php endif; ?>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<?php if ($user['last_login']): ?>
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
<?= date('M d, H:i', strtotime($user['last_login'])) ?>
</div>
<?php else: ?>
<span class="text-gray-400">Never</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="/users/edit?id=<?= $user['id'] ?>" class="text-blue-600 hover:text-blue-800" title="Edit">
<i class="fas fa-edit"></i>
</a>
<?php if ($user['id'] != $_SESSION['user_id']): ?>
<a href="/users/toggle-status?id=<?= $user['id'] ?>"
class="text-orange-600 hover:text-orange-800"
title="<?= $user['is_active'] ? 'Deactivate' : 'Activate' ?>">
<i class="fas fa-<?= $user['is_active'] ? 'user-slash' : 'user-check' ?>"></i>
</a>
<a href="/users/delete?id=<?= $user['id'] ?>"
class="text-red-600 hover:text-red-800"
title="Delete"
onclick="return confirm('Are you sure you want to delete this user?')">
<i class="fas fa-trash"></i>
</a>
<?php else: ?>
<span class="text-gray-400" title="Cannot modify your own account">
<i class="fas fa-lock"></i>
</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<!-- Empty State -->
<div class="p-12 text-center">
<i class="fas fa-users text-gray-300 text-6xl mb-4"></i>
<h3 class="text-lg font-semibold text-gray-700 mb-1">No Users Yet</h3>
<p class="text-sm text-gray-500 mb-4">Start by adding your first user</p>
<a href="/users/create" class="inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-user-plus mr-2"></i>
Add Your First User
</a>
</div>
<?php endif; ?>
</div>
<!-- Pagination Controls -->
<?php if ($pagination['total_pages'] > 1): ?>
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Page Info -->
<div class="text-sm text-gray-600">
Page <span class="font-semibold text-gray-900"><?= $pagination['current_page'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total_pages'] ?></span>
</div>
<!-- Pagination Buttons -->
<div class="flex items-center gap-1">
<?php
// Helper function to build pagination URL
function paginationUrl($page, $filters, $perPage) {
$params = $filters;
$params['page'] = $page;
$params['per_page'] = $perPage;
return '/users?' . http_build_query($params);
}
$currentPage = $pagination['current_page'];
$totalPages = $pagination['total_pages'];
?>
<!-- First Page -->
<?php if ($currentPage > 1): ?>
<a href="<?= paginationUrl(1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-left"></i>
</a>
<?php endif; ?>
<!-- Previous Page -->
<?php if ($currentPage > 1): ?>
<a href="<?= paginationUrl($currentPage - 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-left"></i> Previous
</a>
<?php endif; ?>
<!-- Page Numbers -->
<?php
$range = 2;
$start = max(1, $currentPage - $range);
$end = min($totalPages, $currentPage + $range);
if ($start > 1) {
echo '<a href="' . paginationUrl(1, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">1</a>';
if ($start > 2) {
echo '<span class="px-2 text-gray-500">...</span>';
}
}
for ($i = $start; $i <= $end; $i++) {
if ($i == $currentPage) {
echo '<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">' . $i . '</span>';
} else {
echo '<a href="' . paginationUrl($i, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $i . '</a>';
}
}
if ($end < $totalPages) {
if ($end < $totalPages - 1) {
echo '<span class="px-2 text-gray-500">...</span>';
}
echo '<a href="' . paginationUrl($totalPages, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">' . $totalPages . '</a>';
}
?>
<!-- Next Page -->
<?php if ($currentPage < $totalPages): ?>
<a href="<?= paginationUrl($currentPage + 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
Next <i class="fas fa-angle-right"></i>
</a>
<?php endif; ?>
<!-- Last Page -->
<?php if ($currentPage < $totalPages): ?>
<a href="<?= paginationUrl($totalPages, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<i class="fas fa-angle-double-right"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';
?>