1116 lines
41 KiB
HTML
1116 lines
41 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Reset 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;
|
|
--warning: #f59e0b;
|
|
}
|
|
|
|
* {
|
|
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%
|
|
);
|
|
}
|
|
|
|
.reset-container {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.reset-header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.reset-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);
|
|
}
|
|
|
|
.reset-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;
|
|
}
|
|
|
|
.reset-subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9375rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.reset-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.25rem;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.form-input.error {
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.form-input.valid {
|
|
border-color: var(--success);
|
|
}
|
|
|
|
.password-toggle {
|
|
position: absolute;
|
|
right: 1rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
padding: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.password-toggle:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
/* Password Strength */
|
|
.password-strength {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.strength-bars {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
|
|
.strength-bar {
|
|
flex: 1;
|
|
height: 4px;
|
|
background: var(--border);
|
|
border-radius: 2px;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.strength-bar.active.weak {
|
|
background: var(--error);
|
|
}
|
|
|
|
.strength-bar.active.fair {
|
|
background: var(--warning);
|
|
}
|
|
|
|
.strength-bar.active.good {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.strength-bar.active.strong {
|
|
background: #10b981;
|
|
}
|
|
|
|
.strength-text {
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.strength-text.weak {
|
|
color: var(--error);
|
|
}
|
|
|
|
.strength-text.fair {
|
|
color: var(--warning);
|
|
}
|
|
|
|
.strength-text.good {
|
|
color: #22c55e;
|
|
}
|
|
|
|
.strength-text.strong {
|
|
color: #10b981;
|
|
}
|
|
|
|
/* Password Requirements */
|
|
.password-requirements {
|
|
margin-top: 0.75rem;
|
|
padding: 0.75rem;
|
|
background: var(--bg);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.requirement {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
|
|
.requirement:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.requirement svg {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.requirement.met {
|
|
color: var(--success);
|
|
}
|
|
|
|
.requirement.met svg {
|
|
color: var(--success);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Invalid Token Section */
|
|
.invalid-section {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.invalid-section.visible {
|
|
display: block;
|
|
}
|
|
|
|
.invalid-icon {
|
|
width: 72px;
|
|
height: 72px;
|
|
margin: 0 auto 1.5rem;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--error);
|
|
}
|
|
|
|
.invalid-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.invalid-text {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
line-height: 1.6;
|
|
font-size: 0.9375rem;
|
|
}
|
|
|
|
/* Footer */
|
|
.reset-footer {
|
|
text-align: center;
|
|
margin-top: 1.75rem;
|
|
font-size: 0.9375rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.reset-footer a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.reset-footer a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Password Match Indicator */
|
|
.password-match {
|
|
font-size: 0.75rem;
|
|
margin-top: 0.375rem;
|
|
display: none;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 480px) {
|
|
.reset-container {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.reset-card {
|
|
padding: 1.25rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="reset-container">
|
|
<div class="reset-header">
|
|
<div class="reset-logo">🔐</div>
|
|
<h1 class="reset-title">Reset Password</h1>
|
|
<p class="reset-subtitle">
|
|
Create a new password for your account.
|
|
Make sure it's strong and secure.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="reset-card">
|
|
<!-- Reset Form Section -->
|
|
<div id="reset-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="reset-form"
|
|
hx-post="/api/auth/reset-password"
|
|
hx-target="#reset-response"
|
|
hx-swap="innerHTML"
|
|
hx-indicator="#submit-btn"
|
|
>
|
|
<input type="hidden" name="token" id="reset-token" />
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="password"
|
|
>New Password</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"
|
|
>
|
|
<rect
|
|
x="3"
|
|
y="11"
|
|
width="18"
|
|
height="11"
|
|
rx="2"
|
|
ry="2"
|
|
></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
name="password"
|
|
class="form-input"
|
|
placeholder="••••••••"
|
|
required
|
|
autocomplete="new-password"
|
|
oninput="checkPasswordStrength(this.value)"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="password-toggle"
|
|
onclick="togglePassword('password')"
|
|
>
|
|
<svg
|
|
class="eye-icon"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
></path>
|
|
<circle cx="12" cy="12" r="3"></circle>
|
|
</svg>
|
|
<svg
|
|
class="eye-off-icon"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
style="display: none"
|
|
>
|
|
<path
|
|
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
|
></path>
|
|
<line
|
|
x1="1"
|
|
y1="1"
|
|
x2="23"
|
|
y2="23"
|
|
></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="password-strength" id="password-strength">
|
|
<div class="strength-bars">
|
|
<div class="strength-bar" data-index="0"></div>
|
|
<div class="strength-bar" data-index="1"></div>
|
|
<div class="strength-bar" data-index="2"></div>
|
|
<div class="strength-bar" data-index="3"></div>
|
|
</div>
|
|
<span class="strength-text" id="strength-text"
|
|
>Enter a password</span
|
|
>
|
|
</div>
|
|
<div class="password-requirements">
|
|
<div class="requirement" id="req-length">
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
</svg>
|
|
At least 8 characters
|
|
</div>
|
|
<div class="requirement" id="req-uppercase">
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
</svg>
|
|
One uppercase letter
|
|
</div>
|
|
<div class="requirement" id="req-lowercase">
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
</svg>
|
|
One lowercase letter
|
|
</div>
|
|
<div class="requirement" id="req-number">
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
</svg>
|
|
One number
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="confirm-password"
|
|
>Confirm New Password</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"
|
|
>
|
|
<rect
|
|
x="3"
|
|
y="11"
|
|
width="18"
|
|
height="11"
|
|
rx="2"
|
|
ry="2"
|
|
></rect>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
</svg>
|
|
<input
|
|
type="password"
|
|
id="confirm-password"
|
|
name="confirm_password"
|
|
class="form-input"
|
|
placeholder="••••••••"
|
|
required
|
|
autocomplete="new-password"
|
|
oninput="checkPasswordMatch()"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="password-toggle"
|
|
onclick="togglePassword('confirm-password')"
|
|
>
|
|
<svg
|
|
class="eye-icon"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
></path>
|
|
<circle cx="12" cy="12" r="3"></circle>
|
|
</svg>
|
|
<svg
|
|
class="eye-off-icon"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
style="display: none"
|
|
>
|
|
<path
|
|
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
|
></path>
|
|
<line
|
|
x1="1"
|
|
y1="1"
|
|
x2="23"
|
|
y2="23"
|
|
></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<span class="password-match" id="password-match"></span>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary"
|
|
id="submit-btn"
|
|
>
|
|
<span class="btn-text">Reset Password</span>
|
|
<div class="spinner"></div>
|
|
</button>
|
|
</form>
|
|
|
|
<div id="reset-response"></div>
|
|
</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="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
|
</svg>
|
|
</div>
|
|
<h2 class="success-title">Password Reset!</h2>
|
|
<p class="success-text">
|
|
Your password has been successfully reset.
|
|
You can now sign in with your new password.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick="window.location.href='/auth/login'"
|
|
>
|
|
<span class="btn-text">Sign In</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Invalid Token Section -->
|
|
<div id="invalid-section" class="invalid-section">
|
|
<div class="invalid-icon">
|
|
<svg
|
|
width="36"
|
|
height="36"
|
|
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>
|
|
</div>
|
|
<h2 class="invalid-title">Invalid or Expired Link</h2>
|
|
<p class="invalid-text">
|
|
This password reset link is invalid or has expired.
|
|
Please request a new reset link.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick="window.location.href='/auth/forgot-password'"
|
|
>
|
|
<span class="btn-text">Request New Link</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="reset-footer">
|
|
<p>
|
|
Remember your password?
|
|
<a href="/auth/login">Sign in</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Get token from URL
|
|
function getTokenFromUrl() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get("token");
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const token = getTokenFromUrl();
|
|
|
|
if (!token) {
|
|
showInvalidToken();
|
|
return;
|
|
}
|
|
|
|
document.getElementById("reset-token").value = token;
|
|
|
|
// Optionally verify token with server
|
|
verifyToken(token);
|
|
});
|
|
|
|
// Verify token
|
|
function verifyToken(token) {
|
|
fetch(`/api/auth/verify-reset-token?token=${encodeURIComponent(token)}`)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
showInvalidToken();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// If verification fails, still show form (server will validate on submit)
|
|
});
|
|
}
|
|
|
|
// Password visibility toggle
|
|
function togglePassword(inputId) {
|
|
const input = document.getElementById(inputId);
|
|
const wrapper = input.closest(".input-wrapper");
|
|
const eyeIcon = wrapper.querySelector(".eye-icon");
|
|
const eyeOffIcon = wrapper.querySelector(".eye-off-icon");
|
|
|
|
if (input.type === "password") {
|
|
input.type = "text";
|
|
eyeIcon.style.display = "none";
|
|
eyeOffIcon.style.display = "block";
|
|
} else {
|
|
input.type = "password";
|
|
eyeIcon.style.display = "block";
|
|
eyeOffIcon.style.display = "none";
|
|
}
|
|
}
|
|
|
|
// Password strength checker
|
|
function checkPasswordStrength(password) {
|
|
const bars = document.querySelectorAll(".strength-bar");
|
|
const strengthText = document.getElementById("strength-text");
|
|
|
|
// Check requirements
|
|
const hasLength = password.length >= 8;
|
|
const hasUppercase = /[A-Z]/.test(password);
|
|
const hasLowercase = /[a-z]/.test(password);
|
|
const hasNumber = /[0-9]/.test(password);
|
|
const hasSpecial = /[^A-Za-z0-9]/.test(password);
|
|
|
|
// Update requirement indicators
|
|
updateRequirement("req-length", hasLength);
|
|
updateRequirement("req-uppercase", hasUppercase);
|
|
updateRequirement("req-lowercase", hasLowercase);
|
|
updateRequirement("req-number", hasNumber);
|
|
|
|
// Calculate strength
|
|
let strength = 0;
|
|
if (hasLength) strength++;
|
|
if (hasUppercase) strength++;
|
|
if (hasLowercase) strength++;
|
|
if (hasNumber) strength++;
|
|
if (hasSpecial) strength++;
|
|
if (password.length >= 12) strength++;
|
|
|
|
// Normalize to 4 levels
|
|
let level = 0;
|
|
let levelClass = "";
|
|
let levelText = "Enter a password";
|
|
|
|
if (password.length === 0) {
|
|
levelText = "Enter a password";
|
|
} else if (strength <= 2) {
|
|
level = 1;
|
|
levelClass = "weak";
|
|
levelText = "Weak password";
|
|
} else if (strength === 3) {
|
|
level = 2;
|
|
levelClass = "fair";
|
|
levelText = "Fair password";
|
|
} else if (strength === 4) {
|
|
level = 3;
|
|
levelClass = "good";
|
|
levelText = "Good password";
|
|
} else {
|
|
level = 4;
|
|
levelClass = "strong";
|
|
levelText = "Strong password";
|
|
}
|
|
|
|
// Update bars
|
|
bars.forEach((bar, index) => {
|
|
bar.classList.remove(
|
|
"active",
|
|
"weak",
|
|
"fair",
|
|
"good",
|
|
"strong"
|
|
);
|
|
if (index < level) {
|
|
bar.classList.add("active", levelClass);
|
|
}
|
|
});
|
|
|
|
// Update text
|
|
strengthText.textContent = levelText;
|
|
strengthText.className =
|
|
"strength-text " + (password.length > 0 ? levelClass : "");
|
|
|
|
// Check password match if confirm field has value
|
|
checkPasswordMatch();
|
|
}
|
|
|
|
function updateRequirement(id, met) {
|
|
const req = document.getElementById(id);
|
|
if (met) {
|
|
req.classList.add("met");
|
|
req.querySelector("svg").innerHTML =
|
|
'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline>';
|
|
} else {
|
|
req.classList.remove("met");
|
|
req.querySelector("svg").innerHTML =
|
|
'<circle cx="12" cy="12" r="10"></circle>';
|
|
}
|
|
}
|
|
|
|
function checkPasswordMatch() {
|
|
const password = document.getElementById("password").value;
|
|
const confirmPassword =
|
|
document.getElementById("confirm-password").value;
|
|
const matchIndicator = document.getElementById("password-match");
|
|
const confirmInput = document.getElementById("confirm-password");
|
|
|
|
if (confirmPassword.length === 0) {
|
|
matchIndicator.style.display = "none";
|
|
confirmInput.classList.remove("error", "valid");
|
|
return;
|
|
}
|
|
|
|
matchIndicator.style.display = "block";
|
|
|
|
if (password === confirmPassword) {
|
|
matchIndicator.textContent = "Passwords match";
|
|
matchIndicator.style.color = "var(--success)";
|
|
confirmInput.classList.remove("error");
|
|
confirmInput.classList.add("valid");
|
|
} else {
|
|
matchIndicator.textContent = "Passwords do not match";
|
|
matchIndicator.style.color = "var(--error)";
|
|
confirmInput.classList.remove("valid");
|
|
confirmInput.classList.add("error");
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
document.getElementById("reset-section").style.display = "none";
|
|
document
|
|
.getElementById("success-section")
|
|
.classList.add("visible");
|
|
|
|
// Redirect to login after 3 seconds
|
|
setTimeout(() => {
|
|
window.location.href = "/auth/login";
|
|
}, 3000);
|
|
}
|
|
|
|
// Show invalid token section
|
|
function showInvalidToken() {
|
|
document.getElementById("reset-section").style.display = "none";
|
|
document
|
|
.getElementById("invalid-section")
|
|
.classList.add("visible");
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Form validation
|
|
document
|
|
.getElementById("reset-form")
|
|
.addEventListener("submit", function (e) {
|
|
const password = document.getElementById("password").value;
|
|
const confirmPassword =
|
|
document.getElementById("confirm-password").value;
|
|
|
|
if (password !== confirmPassword) {
|
|
e.preventDefault();
|
|
showError("Passwords do not match");
|
|
return false;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
e.preventDefault();
|
|
showError("Password must be at least 8 characters");
|
|
return false;
|
|
}
|
|
|
|
hideError();
|
|
});
|
|
|
|
// Handle HTMX events
|
|
document.body.addEventListener(
|
|
"htmx:beforeRequest",
|
|
function (event) {
|
|
if (event.target.id === "reset-form") {
|
|
hideError();
|
|
setLoading(true);
|
|
}
|
|
}
|
|
);
|
|
|
|
document.body.addEventListener(
|
|
"htmx:afterRequest",
|
|
function (event) {
|
|
if (event.target.id === "reset-form") {
|
|
setLoading(false);
|
|
|
|
if (event.detail.successful) {
|
|
try {
|
|
const response = JSON.parse(
|
|
event.detail.xhr.responseText
|
|
);
|
|
if (response.success) {
|
|
showSuccess();
|
|
} else if (response.invalid_token) {
|
|
showInvalidToken();
|
|
}
|
|
} catch (e) {
|
|
if (event.detail.xhr.status === 200) {
|
|
showSuccess();
|
|
}
|
|
}
|
|
} else {
|
|
try {
|
|
const response = JSON.parse(
|
|
event.detail.xhr.responseText
|
|
);
|
|
if (response.invalid_token) {
|
|
showInvalidToken();
|
|
} else {
|
|
showError(
|
|
response.error ||
|
|
"Failed to reset password. Please try again."
|
|
);
|
|
}
|
|
} catch (e) {
|
|
showError(
|
|
"Failed to reset password. Please try again."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Clear error when user starts typing
|
|
document.querySelectorAll(".form-input").forEach((input) => {
|
|
input.addEventListener("input", hideError);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|