botui/ui/suite/auth/forgot-password.html

741 lines
25 KiB
HTML
Raw Normal View History

2025-12-06 11:09:12 -03:00
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forgot Password - General Bots</title>
<script src="/js/vendor/htmx.min.js"></script>
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.1);
--bg: #0f172a;
--surface: #1e293b;
--surface-hover: #334155;
--border: #334155;
--text: #f8fafc;
--text-secondary: #94a3b8;
--error: #ef4444;
--success: #22c55e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image:
radial-gradient(
ellipse at top,
rgba(59, 130, 246, 0.1) 0%,
transparent 50%
),
radial-gradient(
ellipse at bottom,
rgba(139, 92, 246, 0.1) 0%,
transparent 50%
);
}
.forgot-container {
width: 100%;
max-width: 420px;
padding: 2rem;
}
.forgot-header {
text-align: center;
margin-bottom: 2rem;
}
.forgot-logo {
width: 72px;
height: 72px;
margin: 0 auto 1.25rem;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.3);
}
.forgot-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(
135deg,
var(--text),
var(--text-secondary)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.forgot-subtitle {
color: var(--text-secondary);
font-size: 0.9375rem;
line-height: 1.5;
}
.forgot-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.75rem;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
}
/* Message Box */
.message-box {
padding: 0.875rem 1rem;
border-radius: 10px;
margin-bottom: 1.25rem;
font-size: 0.875rem;
display: none;
align-items: flex-start;
gap: 0.625rem;
}
.message-box.visible {
display: flex;
}
.message-box.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--error);
}
.message-box.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: var(--success);
}
.message-box svg {
flex-shrink: 0;
margin-top: 0.125rem;
}
/* Form Styles */
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text);
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
pointer-events: none;
}
.form-input {
width: 100%;
padding: 0.875rem 1rem 0.875rem 2.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-size: 1rem;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.form-input::placeholder {
color: var(--text-secondary);
}
/* Buttons */
.btn {
width: 100%;
padding: 0.875rem 1.25rem;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
position: relative;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), #6366f1);
color: white;
border: none;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-text {
transition: opacity 0.2s ease;
}
.btn.loading .btn-text {
opacity: 0;
}
.btn.loading .spinner {
display: block;
}
.spinner {
display: none;
position: absolute;
width: 22px;
height: 22px;
border: 2px solid transparent;
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Back Link */
.back-link {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1.5rem;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9375rem;
transition: color 0.2s ease;
}
.back-link:hover {
color: var(--primary);
}
/* Success Section */
.success-section {
display: none;
text-align: center;
padding: 1rem 0;
}
.success-section.visible {
display: block;
}
.success-icon {
width: 72px;
height: 72px;
margin: 0 auto 1.5rem;
background: rgba(34, 197, 94, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--success);
}
.success-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
.success-text {
color: var(--text-secondary);
margin-bottom: 1.5rem;
line-height: 1.6;
font-size: 0.9375rem;
}
.success-email {
color: var(--primary);
font-weight: 500;
}
.email-tips {
background: var(--bg);
border-radius: 10px;
padding: 1rem;
margin-top: 1.5rem;
text-align: left;
}
.email-tips-title {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.625rem;
}
.email-tips-list {
list-style: none;
font-size: 0.8125rem;
color: var(--text-secondary);
}
.email-tips-list li {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.375rem;
}
.email-tips-list li:last-child {
margin-bottom: 0;
}
.email-tips-list svg {
flex-shrink: 0;
color: var(--primary);
}
.resend-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
text-align: center;
}
.resend-text {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
.resend-btn {
background: none;
border: none;
color: var(--primary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
padding: 0;
}
.resend-btn:hover {
text-decoration: underline;
}
.resend-btn:disabled {
color: var(--text-secondary);
cursor: not-allowed;
text-decoration: none;
}
/* Footer */
.forgot-footer {
text-align: center;
margin-top: 1.75rem;
font-size: 0.9375rem;
color: var(--text-secondary);
}
.forgot-footer a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.forgot-footer a:hover {
text-decoration: underline;
}
/* Responsive */
@media (max-width: 480px) {
.forgot-container {
padding: 1rem;
}
.forgot-card {
padding: 1.25rem;
}
}
</style>
</head>
<body>
<div class="forgot-container">
<div class="forgot-header">
<div class="forgot-logo">🔑</div>
<h1 class="forgot-title">Forgot Password?</h1>
<p class="forgot-subtitle">
No worries! Enter your email address and we'll send you a
link to reset your password.
</p>
</div>
<div class="forgot-card">
<!-- Request Form Section -->
<div id="request-section">
<div class="message-box error" id="error-message">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<span id="error-text">An error occurred</span>
</div>
<form
id="forgot-form"
hx-post="/api/auth/forgot-password"
hx-target="#forgot-response"
hx-swap="innerHTML"
hx-indicator="#submit-btn"
>
<div class="form-group">
<label class="form-label" for="email"
>Email Address</label
>
<div class="input-wrapper">
<svg
class="input-icon"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="you@example.com"
required
autocomplete="email"
/>
</div>
</div>
<button
type="submit"
class="btn btn-primary"
id="submit-btn"
>
<span class="btn-text">Send Reset Link</span>
<div class="spinner"></div>
</button>
</form>
<div id="forgot-response"></div>
<a href="/auth/login" class="back-link">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Login
</a>
</div>
<!-- Success Section -->
<div id="success-section" class="success-section">
<div class="success-icon">
<svg
width="36"
height="36"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<h2 class="success-title">Check Your Email</h2>
<p class="success-text">
We've sent a password reset link to<br />
<span class="success-email" id="success-email"
>your@email.com</span
>
</p>
<div class="email-tips">
<div class="email-tips-title">
Didn't receive the email?
</div>
<ul class="email-tips-list">
<li>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline
points="9 11 12 14 22 4"
></polyline>
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
></path>
</svg>
Check your spam or junk folder
</li>
<li>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline
points="9 11 12 14 22 4"
></polyline>
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
></path>
</svg>
Make sure you entered the correct email
</li>
<li>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline
points="9 11 12 14 22 4"
></polyline>
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
></path>
</svg>
The link expires in 1 hour
</li>
</ul>
</div>
<div class="resend-section">
<p class="resend-text">Still no email?</p>
<button
type="button"
class="resend-btn"
id="resend-btn"
onclick="resendEmail()"
>
Resend reset link
</button>
</div>
<a href="/auth/login" class="back-link">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to Login
</a>
</div>
</div>
<div class="forgot-footer">
<p>
Remember your password?
<a href="/auth/login">Sign in</a>
</p>
</div>
</div>
<script>
let submittedEmail = "";
let resendCooldown = 0;
// Show error message
function showError(message) {
const errorBox = document.getElementById("error-message");
const errorText = document.getElementById("error-text");
errorText.textContent = message;
errorBox.classList.add("visible");
}
// Hide error message
function hideError() {
document
.getElementById("error-message")
.classList.remove("visible");
}
// Show success section
function showSuccess(email) {
submittedEmail = email;
document.getElementById("request-section").style.display =
"none";
document
.getElementById("success-section")
.classList.add("visible");
document.getElementById("success-email").textContent = email;
}
// Loading state
function setLoading(loading) {
const btn = document.getElementById("submit-btn");
if (loading) {
btn.classList.add("loading");
btn.disabled = true;
} else {
btn.classList.remove("loading");
btn.disabled = false;
}
}
// Resend email with cooldown
function resendEmail() {
if (resendCooldown > 0) return;
fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: submittedEmail }),
});
// Start cooldown
resendCooldown = 60;
const resendBtn = document.getElementById("resend-btn");
resendBtn.disabled = true;
const interval = setInterval(() => {
resendCooldown--;
resendBtn.textContent = `Resend reset link (${resendCooldown}s)`;
if (resendCooldown <= 0) {
clearInterval(interval);
resendBtn.textContent = "Resend reset link";
resendBtn.disabled = false;
}
}, 1000);
}
// Handle HTMX events
document.body.addEventListener(
"htmx:beforeRequest",
function (event) {
if (event.target.id === "forgot-form") {
hideError();
setLoading(true);
}
},
);
document.body.addEventListener(
"htmx:afterRequest",
function (event) {
if (event.target.id === "forgot-form") {
setLoading(false);
if (event.detail.successful) {
const email = document.getElementById("email").value;
showSuccess(email);
} else {
try {
const response = JSON.parse(
event.detail.xhr.responseText,
);
showError(
response.error ||
"Failed to send reset link. Please try again.",
);
} catch (e) {
// Even if there's an error, for security we might show success
// to prevent email enumeration
const email = document.getElementById("email").value;
showSuccess(email);
}
}
}
},
);
// Clear error when user starts typing
document.getElementById("email").addEventListener("input", hideError);
</script>
</body>
</html>