<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password Change Required - MCP Gateway</title>
<!-- Tailwind CSS -->
{% if ui_airgapped %}
<script src="{{ root_path }}/static/vendor/tailwindcss/tailwind.min.js"></script>
{% else %}
<script src="https://cdn.tailwindcss.com"></script>
{% endif %}
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
animation: {
"gradient-x": "gradient-x 15s ease infinite",
float: "float 6s ease-in-out infinite",
"pulse-soft": "pulse-soft 2s ease-in-out infinite",
"slide-up": "slide-up 0.8s ease-out",
"fade-in": "fade-in 1s ease-out",
"scale-pulse": "scale-pulse 4s ease-in-out infinite",
},
keyframes: {
"gradient-x": {
"0%, 100%": {
"background-size": "200% 200%",
"background-position": "left center",
},
"50%": {
"background-size": "200% 200%",
"background-position": "right center",
},
},
float: {
"0%, 100%": { transform: "translateY(0px)" },
"50%": { transform: "translateY(-20px)" },
},
"pulse-soft": {
"0%, 100%": { opacity: 1 },
"50%": { opacity: 0.8 },
},
"slide-up": {
"0%": { transform: "translateY(30px)", opacity: 0 },
"100%": { transform: "translateY(0)", opacity: 1 },
},
"fade-in": {
"0%": { opacity: 0 },
"100%": { opacity: 1 },
},
"scale-pulse": {
"0%, 100%": { transform: "scale(1)" },
"50%": { transform: "scale(1.05)" },
},
},
},
},
};
</script>
<!-- Font Awesome -->
{% if ui_airgapped %}
<link rel="stylesheet" href="{{ root_path }}/static/vendor/fontawesome/css/all.min.css" />
{% else %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
{% endif %}
</head>
<body class="h-full bg-gray-50 dark:bg-gray-900 overflow-hidden">
<!-- Main Split Layout -->
<div class="min-h-screen flex">
<!-- Left Side - Password Change Form -->
<div
class="flex-1 flex flex-col justify-center py-6 px-4 sm:px-6 lg:px-8 bg-white dark:bg-gray-900 relative overflow-hidden"
>
<!-- Subtle background decoration -->
<div
class="absolute inset-0 bg-gradient-to-br from-purple-50/50 to-blue-100/30 dark:from-gray-800/50 dark:to-gray-700/30"
></div>
<div
class="absolute top-10 left-10 w-32 h-32 bg-gradient-to-br from-purple-200/30 to-blue-200/30 dark:from-purple-800/30 dark:to-blue-800/30 rounded-full animate-float"
></div>
<div
class="absolute bottom-20 right-10 w-24 h-24 bg-gradient-to-br from-red-200/30 to-pink-200/30 dark:from-red-800/30 dark:to-pink-800/30 rounded-full animate-float"
style="animation-delay: 2s"
></div>
<div class="sm:mx-auto sm:w-full sm:max-w-md relative z-10">
<!-- Password Change Required Card -->
<div
class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-xl py-8 px-6 shadow-2xl rounded-2xl border border-gray-200/50 dark:border-gray-700/50 animate-slide-up"
style="animation-delay: 0.2s"
>
<!-- Header with icon -->
<div class="text-center mb-6">
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-gradient-to-r from-purple-400 to-indigo-500 mb-4">
<i class="fas fa-key text-white text-xl"></i>
</div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
Password Change Required
</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Your password has expired and must be changed to continue.
</p>
<p class="mt-1 text-xs text-gray-400">Password policy enabled: <strong>{{ password_policy_enabled }}</strong></p>
</div>
<!-- Error/Success messages -->
<div
id="error-message"
class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 rounded-xl text-sm hidden"
>
<div class="flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
<span id="error-text">An error occurred while changing your password. Please try again.</span>
</div>
</div>
<div
id="success-message"
class="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400 rounded-xl text-sm hidden"
>
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span id="success-text">Password changed successfully! Redirecting...</span>
</div>
</div>
<!-- Password Change Form -->
<form
class="space-y-6"
id="password-change-form"
action="{{ root_path }}/admin/change-password-required"
method="post"
>
<div>
<label
for="current_password"
class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"
>
<i class="fas fa-lock mr-2 text-gray-400"></i>Current Password
</label>
<div class="relative">
<input
id="current_password"
name="current_password"
type="password"
autocomplete="current-password"
required
placeholder="Enter your current password"
class="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700/50 dark:text-white transition-all duration-200 backdrop-blur-sm pr-10"
/>
<button
type="button"
onclick="togglePasswordVisibility('current_password')"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<i id="current_password_icon" class="fas fa-eye"></i>
</button>
</div>
</div>
<div>
<label
for="new_password"
class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"
>
<i class="fas fa-key mr-2 text-gray-400"></i>New Password
</label>
<div class="relative">
<input
id="new_password"
name="new_password"
type="password"
autocomplete="new-password"
required
placeholder="Enter your new password"
class="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700/50 dark:text-white transition-all duration-200 backdrop-blur-sm pr-10"
oninput="validatePassword()"
/>
<button
type="button"
onclick="togglePasswordVisibility('new_password')"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<i id="new_password_icon" class="fas fa-eye"></i>
</button>
</div>
<div class="mt-2">
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Password strength: <span id="password-strength" class="font-medium">Medium</span></p>
</div>
</div>
<div>
<label
for="confirm_password"
class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"
>
<i class="fas fa-lock mr-2 text-gray-400"></i>Confirm New Password
</label>
<div class="relative">
<input
id="confirm_password"
name="confirm_password"
type="password"
autocomplete="new-password"
required
placeholder="Confirm your new password"
class="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700/50 dark:text-white transition-all duration-200 backdrop-blur-sm pr-10"
oninput="validatePassword()"
/>
<button
type="button"
onclick="togglePasswordVisibility('confirm_password')"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<i id="confirm_password_icon" class="fas fa-eye"></i>
</button>
</div>
<div class="mt-1">
<p id="password-match" class="text-xs text-green-600 dark:text-green-400 hidden">
<i class="fas fa-check mr-1"></i>Passwords match
</p>
</div>
</div>
<!-- Password Requirements (only show when policy enabled) -->
{% if password_policy_enabled %}
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
<h3 class="text-sm font-semibold text-blue-700 dark:text-blue-300 mb-2">
<i class="fas fa-info-circle mr-2"></i>Password Requirements
</h3>
<ul class="text-xs text-blue-600 dark:text-blue-400 space-y-1">
<li id="req-length" class="flex items-center">
<i class="fas fa-circle text-gray-400 mr-2"></i>
At least {{ password_min_length or 8 }} characters long
</li>
{% if password_require_uppercase %}
<li id="req-uppercase" class="flex items-center">
<i class="fas fa-circle text-gray-400 mr-2"></i>
Contains uppercase letters (A-Z)
</li>
{% endif %}
{% if password_require_lowercase %}
<li id="req-lowercase" class="flex items-center">
<i class="fas fa-circle text-gray-400 mr-2"></i>
Contains lowercase letters (a-z)
</li>
{% endif %}
{% if password_require_numbers %}
<li id="req-numbers" class="flex items-center">
<i class="fas fa-circle text-gray-400 mr-2"></i>
Contains numbers (0-9)
</li>
{% endif %}
{% if password_require_special %}
<li id="req-special" class="flex items-center">
<i class="fas fa-circle text-gray-400 mr-2"></i>
Contains special characters (!@#$%^&*()_+[]{}:;"'<>?,.)
</li>
{% endif %}
</ul>
</div>
{% endif %}
<button
type="submit"
id="submit-btn"
class="w-full flex justify-center items-center py-3 px-4 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-all duration-200"
>
<i class="fas fa-key mr-2"></i>
Change Password
</button>
</form>
<!-- Footer -->
<div class="mt-6 text-center">
<p class="text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-shield-alt mr-1"></i>
Your password will be securely encrypted and stored
</p>
</div>
</div>
</div>
</div>
<!-- Right Side - Security Features -->
<div
class="flex-1 bg-gradient-to-br from-purple-600 via-indigo-600 to-blue-700 dark:from-purple-900 dark:via-indigo-900 dark:to-blue-900 relative overflow-hidden flex items-center justify-center"
>
<!-- Integrated background pattern -->
<div class="absolute inset-0 opacity-10">
<div
class="absolute inset-0"
style="
background-image: url('data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M20 20c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10zm10 0c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10z"/%3E%3C/g%3E%3C/svg%3E');
"
></div>
</div>
<!-- Subtle floating accents -->
<div
class="absolute top-16 right-16 w-2 h-2 bg-white/20 rounded-full animate-pulse-soft"
></div>
<div
class="absolute bottom-24 left-16 w-1 h-1 bg-white/30 rounded-full animate-pulse-soft"
style="animation-delay: 1s"
></div>
<div
class="absolute top-1/3 right-32 w-1.5 h-1.5 bg-white/25 rounded-full animate-pulse-soft"
style="animation-delay: 2s"
></div>
<!-- Main content container -->
<div class="relative z-10 max-w-2xl px-6 py-4">
<!-- Compact Hero -->
<div class="text-center mb-4">
<div class="flex items-center justify-center mb-3">
<a
href="https://github.com/IBM/mcp-context-forge"
target="_blank"
class="hover:opacity-80 transition-opacity"
>
<img
src="{{ root_path }}/static/contextforge-logo-white.png"
alt="ContextForge"
class="h-12"
/>
</a>
</div>
<p class="text-lg font-semibold text-white mb-1">
MCP & AI Gateway
</p>
<p class="text-sm text-red-100 opacity-90">
MCP, A2A and REST gateway with advanced security & observability
</p>
</div>
<!-- Security Features -->
<div class="space-y-6">
<!-- Password Security -->
<div>
<h3
class="text-xs font-semibold text-red-200 uppercase tracking-wide mb-3 text-center"
>
SECURITY FEATURES
</h3>
<div class="grid grid-cols-1 gap-4">
<div
class="bg-white/8 backdrop-blur-sm rounded-lg p-4 border border-white/10 hover:bg-white/12 transition-all duration-300 group"
>
<div
class="w-8 h-8 bg-gradient-to-br from-green-400 to-emerald-500 rounded-lg flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"
>
<i class="fas fa-lock text-white text-sm"></i>
</div>
<h4 class="text-sm font-semibold text-white mb-2">
Password Security
</h4>
<p class="text-xs text-red-100 leading-relaxed">
Strong password policies with automatic expiration and complexity requirements
</p>
</div>
<div
class="bg-white/8 backdrop-blur-sm rounded-lg p-4 border border-white/10 hover:bg-white/12 transition-all duration-300 group"
>
<div
class="w-8 h-8 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-lg flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"
>
<i class="fas fa-shield-alt text-white text-sm"></i>
</div>
<h4 class="text-sm font-semibold text-white mb-2">
Account Protection
</h4>
<p class="text-xs text-red-100 leading-relaxed">
Multi-factor authentication and account lockout protection
</p>
</div>
<div
class="bg-white/8 backdrop-blur-sm rounded-lg p-4 border border-white/10 hover:bg-white/12 transition-all duration-300 group"
>
<div
class="w-8 h-8 bg-gradient-to-br from-purple-400 to-pink-500 rounded-lg flex items-center justify-center mb-3 group-hover:scale-110 transition-transform"
>
<i class="fas fa-history text-white text-sm"></i>
</div>
<h4 class="text-sm font-semibold text-white mb-2">
Audit & Monitoring
</h4>
<p class="text-xs text-red-100 leading-relaxed">
Complete audit trail of all authentication events and activities
</p>
</div>
</div>
</div>
</div>
<!-- Bottom accent -->
<div class="mt-6 text-center">
<div class="inline-flex items-center space-x-4 text-red-100/80">
<div class="flex items-center space-x-1">
<i class="fas fa-check-circle text-green-300 text-xs"></i>
<span class="text-xs">Secure by Design</span>
</div>
<div class="flex items-center space-x-1">
<i class="fas fa-clock text-yellow-300 text-xs"></i>
<span class="text-xs">Enterprise Ready</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Initialize ROOT_PATH for JavaScript URL composition
window.ROOT_PATH = {{ root_path | tojson }};
// Initialize page
document.addEventListener("DOMContentLoaded", function () {
setupFormValidation();
setupPasswordToggles();
handleErrorMessages();
});
// Setup form validation
function setupFormValidation() {
const form = document.getElementById('password-change-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
showError('Passwords do not match');
return;
}
if (!isPasswordValid(newPassword)) {
showError('Password does not meet requirements');
return;
}
// Show loading state
const submitBtn = document.getElementById('submit-btn');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Changing Password...';
submitBtn.disabled = true;
// Submit form
form.submit();
});
}
// Setup password toggle functionality
function setupPasswordToggles() {
// Add event listeners for password visibility toggles
}
// Toggle password visibility
function togglePasswordVisibility(fieldId) {
const passwordField = document.getElementById(fieldId);
const passwordIcon = document.getElementById(fieldId + '_icon');
if (passwordField.type === 'password') {
passwordField.type = 'text';
passwordIcon.classList.remove('fa-eye');
passwordIcon.classList.add('fa-eye-slash');
} else {
passwordField.type = 'password';
passwordIcon.classList.remove('fa-eye-slash');
passwordIcon.classList.add('fa-eye');
}
}
// Password policy settings from server
const passwordPolicy = {
enabled: {{ password_policy_enabled | tojson }},
minLength: {{ password_min_length | tojson }} || 8,
requireUppercase: {{ password_require_uppercase | tojson }},
requireLowercase: {{ password_require_lowercase | tojson }},
requireNumbers: {{ password_require_numbers | tojson }},
requireSpecial: {{ password_require_special | tojson }}
};
// Validate password requirements
function validatePassword() {
// If policy disabled, skip requirement checks and only update match
if (!passwordPolicy.enabled) {
validatePasswordMatch();
const strengthElement = document.getElementById('password-strength');
if (strengthElement) {
strengthElement.textContent = 'Disabled';
strengthElement.className = 'font-medium text-gray-500';
}
return;
}
const password = document.getElementById('new_password').value;
const strength = getPasswordStrength(password);
// Update password strength indicator
const strengthElement = document.getElementById('password-strength');
strengthElement.textContent = strength.label;
strengthElement.className = `font-medium ${strength.color}`;
// Update requirement indicators (only for elements that exist)
updateRequirement('req-length', password.length >= passwordPolicy.minLength);
if (passwordPolicy.requireUppercase) updateRequirement('req-uppercase', /[A-Z]/.test(password));
if (passwordPolicy.requireLowercase) updateRequirement('req-lowercase', /[a-z]/.test(password));
if (passwordPolicy.requireNumbers) updateRequirement('req-numbers', /[0-9]/.test(password));
if (passwordPolicy.requireSpecial) updateRequirement('req-special', /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>\/?]/.test(password));
// Check password match after validation
validatePasswordMatch();
}
// Get password strength
function getPasswordStrength(password) {
let score = 0;
if (password.length >= passwordPolicy.minLength) score++;
if (/[A-Z]/.test(password)) score++;
if (/[a-z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) score++;
if (score <= 2) return { label: 'Weak', color: 'text-red-500' };
if (score <= 4) return { label: 'Medium', color: 'text-yellow-500' };
return { label: 'Strong', color: 'text-green-500' };
}
// Update requirement indicator (only if element exists)
function updateRequirement(id, met) {
const element = document.getElementById(id);
if (!element) return;
const icon = element.querySelector('i');
if (!icon) return;
if (met) {
icon.className = 'fas fa-check-circle text-green-500 mr-2';
element.classList.remove('text-blue-600');
element.classList.add('text-green-600');
} else {
icon.className = 'fas fa-circle text-gray-400 mr-2';
element.classList.remove('text-green-600');
element.classList.add('text-blue-600');
}
}
// Validate password match
function validatePasswordMatch() {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
const matchElement = document.getElementById('password-match');
if (confirmPassword && newPassword === confirmPassword) {
matchElement.classList.remove('hidden');
} else {
matchElement.classList.add('hidden');
}
}
// Check if password is valid based on policy
function isPasswordValid(password) {
if (!passwordPolicy.enabled) return true;
const lengthOk = password.length >= passwordPolicy.minLength;
const uppercaseOk = !passwordPolicy.requireUppercase || /[A-Z]/.test(password);
const lowercaseOk = !passwordPolicy.requireLowercase || /[a-z]/.test(password);
const numbersOk = !passwordPolicy.requireNumbers || /[0-9]/.test(password);
const specialOk = !passwordPolicy.requireSpecial || /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>\/?]/.test(password);
return lengthOk && uppercaseOk && lowercaseOk && numbersOk && specialOk;
}
// Handle error messages from URL parameters
function handleErrorMessages() {
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
let errorMessage = 'Password change failed';
switch (error) {
case 'invalid_password':
errorMessage = 'Current password is incorrect';
break;
case 'weak_password':
errorMessage = 'New password does not meet requirements';
break;
case 'same_password':
errorMessage = 'New password must be different from current password';
break;
case 'mismatch':
errorMessage = 'Password confirmation does not match';
break;
default:
errorMessage = error.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
showError(errorMessage);
}
}
// Show error message
function showError(message) {
const errorDiv = document.getElementById('error-message');
const errorText = document.getElementById('error-text');
errorText.textContent = message;
errorDiv.classList.remove('hidden');
// Auto-hide after 8 seconds
setTimeout(() => {
errorDiv.classList.add('hidden');
}, 8000);
}
// Show success message
function showSuccess(message) {
const successDiv = document.getElementById('success-message');
const successText = document.getElementById('success-text');
successText.textContent = message;
successDiv.classList.remove('hidden');
}
</script>
</body>
</html>