botserver/templates/auth/login.html

731 lines
28 KiB
HTML
Raw Normal View History

<!doctype html>
2025-11-29 16:29:28 -03:00
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - BotServer</title>
<script src="/static/js/vendor/htmx.min.js"></script>
<script src="/static/js/vendor/htmx-ws.js"></script>
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--secondary: #64748b;
--background: #ffffff;
--surface: #f8fafc;
--text: #1e293b;
--text-secondary: #64748b;
--border: #e2e8f0;
--error: #ef4444;
--success: #10b981;
}
2025-11-29 16:29:28 -03:00
[data-theme="dark"] {
--background: #0f172a;
--surface: #1e293b;
--text: #f1f5f9;
--text-secondary: #94a3b8;
--border: #334155;
}
2025-11-29 16:29:28 -03:00
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
2025-11-29 16:29:28 -03:00
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, sans-serif;
background: var(--background);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 2rem;
}
.login-card {
background: var(--surface);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
border: 1px solid var(--border);
}
.logo {
text-align: center;
margin-bottom: 2rem;
}
.logo-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.logo-text {
font-size: 1.5rem;
font-weight: 600;
color: var(--text);
}
h1 {
font-size: 1.25rem;
font-weight: 600;
text-align: center;
margin-bottom: 1.5rem;
color: var(--text);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
}
input[type="email"],
input[type="password"],
input[type="text"] {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 1rem;
background: var(--background);
color: var(--text);
transition: all 0.2s ease;
}
input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
}
input[type="checkbox"] {
margin-right: 0.5rem;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
.checkbox-label {
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
}
.btn {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.divider {
display: flex;
align-items: center;
margin: 1.5rem 0;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.divider span {
padding: 0 1rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
.oauth-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.btn-oauth {
background: var(--background);
color: var(--text);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
font-size: 0.875rem;
text-decoration: none;
}
.btn-oauth:hover {
background: var(--surface);
border-color: var(--primary);
}
.btn-oauth.hidden {
display: none;
}
.btn-oauth svg {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
/* Provider-specific colors on hover */
.btn-oauth-google:hover {
border-color: #ea4335;
color: #ea4335;
}
.btn-oauth-discord:hover {
border-color: #5865f2;
color: #5865f2;
}
.btn-oauth-reddit:hover {
border-color: #ff4500;
color: #ff4500;
}
.btn-oauth-twitter:hover {
border-color: #1da1f2;
color: #1da1f2;
}
.btn-oauth-microsoft:hover {
border-color: #00a4ef;
color: #00a4ef;
}
.btn-oauth-facebook:hover {
border-color: #1877f2;
color: #1877f2;
}
.error-message {
background: rgb(239 68 68 / 0.1);
color: var(--error);
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.875rem;
border: 1px solid rgb(239 68 68 / 0.2);
}
.success-message {
background: rgb(16 185 129 / 0.1);
color: var(--success);
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.875rem;
border: 1px solid rgb(16 185 129 / 0.2);
}
.loading-spinner {
display: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin: 0 auto;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.htmx-request .loading-spinner {
display: inline-block;
}
.htmx-request .btn-text {
display: none;
}
.link {
color: var(--primary);
text-decoration: none;
font-size: 0.875rem;
}
.link:hover {
text-decoration: underline;
}
.footer-links {
text-align: center;
margin-top: 1.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
2025-11-29 16:29:28 -03:00
.theme-toggle {
position: absolute;
top: 1rem;
right: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem;
cursor: pointer;
font-size: 1.25rem;
transition: all 0.2s ease;
}
.theme-toggle:hover {
background: var(--background);
}
.dev-mode-banner {
background: rgb(251 146 60 / 0.1);
color: rgb(234 88 12);
padding: 0.5rem;
text-align: center;
font-size: 0.875rem;
border-bottom: 1px solid rgb(251 146 60 / 0.2);
position: fixed;
top: 0;
left: 0;
right: 0;
}
.oauth-section {
margin-top: 0.5rem;
}
.oauth-loading {
text-align: center;
padding: 1rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
.no-oauth {
text-align: center;
padding: 0.5rem;
color: var(--text-secondary);
font-size: 0.75rem;
}
</style>
</head>
<body>
<!-- Theme Toggle -->
<button
class="theme-toggle"
onclick="toggleTheme()"
aria-label="Toggle theme"
>
<span id="theme-icon">🌙</span>
</button>
<!-- Dev Mode Banner (shown when Zitadel is not available) -->
<div id="dev-mode-banner" class="dev-mode-banner" style="display: none">
⚠️ Development Mode: Use any email with password "password"
</div>
<div class="login-container">
<div class="login-card">
<!-- Logo -->
<div class="logo">
<div class="logo-icon">🤖</div>
<div class="logo-text">BotServer</div>
2025-11-29 16:29:28 -03:00
</div>
<h1>Sign in to your account</h1>
2025-11-29 16:29:28 -03:00
<!-- Error Message -->
{% if error_message %}
<div class="error-message">{{ error_message }}</div>
{% endif %}
2025-11-29 16:29:28 -03:00
<!-- Success Message Target -->
<div id="message-container"></div>
<!-- Login Form -->
<form
id="login-form"
hx-post="/auth/login"
hx-target="#message-container"
hx-indicator=".loading-spinner"
>
<div class="form-group">
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
placeholder="user@example.com"
/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
required
autocomplete="current-password"
placeholder="Enter your password"
/>
</div>
<div class="checkbox-group">
<input
type="checkbox"
id="remember_me"
name="remember_me"
value="true"
/>
<label for="remember_me" class="checkbox-label"
>Remember me</label
>
</div>
{% if redirect_url %}
<input
type="hidden"
name="redirect"
value="{{ redirect_url }}"
/>
{% endif %}
<button type="submit" class="btn btn-primary">
<span class="btn-text">Sign in</span>
<span class="loading-spinner"></span>
</button>
</form>
<!-- OAuth Options -->
<div class="divider">
<span>or continue with</span>
</div>
2025-11-29 16:29:28 -03:00
<div class="oauth-section" id="oauth-section">
<div class="oauth-loading" id="oauth-loading">
Loading login options...
</div>
<div
class="oauth-grid"
id="oauth-buttons"
style="display: none"
>
<!-- Google -->
<a
href="/auth/oauth/google{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-google hidden"
id="btn-google"
title="Sign in with Google"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Google
</a>
<!-- Microsoft -->
<a
href="/auth/oauth/microsoft{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-microsoft hidden"
id="btn-microsoft"
title="Sign in with Microsoft"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.4 24H0V12.6h11.4V24z"
fill="#00A4EF"
/>
<path
d="M24 24H12.6V12.6H24V24z"
fill="#FFB900"
/>
<path
d="M11.4 11.4H0V0h11.4v11.4z"
fill="#F25022"
/>
<path
d="M24 11.4H12.6V0H24v11.4z"
fill="#7FBA00"
/>
</svg>
Microsoft
</a>
<!-- Discord -->
<a
href="/auth/oauth/discord{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-discord hidden"
id="btn-discord"
title="Sign in with Discord"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
fill="#5865F2"
/>
</svg>
Discord
</a>
<!-- Facebook -->
<a
href="/auth/oauth/facebook{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-facebook hidden"
id="btn-facebook"
title="Sign in with Facebook"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"
fill="#1877F2"
/>
</svg>
Facebook
</a>
<!-- Twitter/X -->
<a
href="/auth/oauth/twitter{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-twitter hidden"
id="btn-twitter"
title="Sign in with Twitter/X"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
Twitter
</a>
<!-- Reddit -->
<a
href="/auth/oauth/reddit{% if redirect_url %}?redirect={{ redirect_url }}{% endif %}"
class="btn btn-oauth btn-oauth-reddit hidden"
id="btn-reddit"
title="Sign in with Reddit"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
fill="#FF4500"
/>
</svg>
Reddit
</a>
</div>
<div class="no-oauth" id="no-oauth" style="display: none">
No social login providers configured
</div>
</div>
<!-- Footer Links -->
<div class="footer-links">
<a href="/auth/forgot-password" class="link"
>Forgot password?</a
>
<span> · </span>
<a href="/auth/register" class="link">Create account</a>
</div>
2025-11-29 16:29:28 -03:00
</div>
</div>
<script>
// Theme management
function initTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
updateThemeIcon(savedTheme);
}
function toggleTheme() {
const currentTheme =
document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
updateThemeIcon(newTheme);
}
function updateThemeIcon(theme) {
const icon = document.getElementById("theme-icon");
icon.textContent = theme === "light" ? "🌙" : "☀️";
}
// Check if in development mode
async function checkDevMode() {
try {
const response = await fetch("/api/auth/mode");
const data = await response.json();
if (data.mode === "development") {
document.getElementById(
"dev-mode-banner",
).style.display = "block";
document.body.style.paddingTop = "2.5rem";
}
} catch (err) {
// Ignore - assume production mode
}
}
// Load enabled OAuth providers
async function loadOAuthProviders() {
const loadingEl = document.getElementById("oauth-loading");
const buttonsEl = document.getElementById("oauth-buttons");
const noOAuthEl = document.getElementById("no-oauth");
try {
const response = await fetch("/auth/oauth/providers");
const data = await response.json();
loadingEl.style.display = "none";
if (data.providers && data.providers.length > 0) {
buttonsEl.style.display = "grid";
// Show buttons for enabled providers
data.providers.forEach((provider) => {
const btn = document.getElementById(
"btn-" + provider.id,
);
if (btn) {
btn.classList.remove("hidden");
}
});
// Check if any buttons are visible
const visibleButtons =
buttonsEl.querySelectorAll("a:not(.hidden)");
if (visibleButtons.length === 0) {
buttonsEl.style.display = "none";
noOAuthEl.style.display = "block";
}
} else {
noOAuthEl.style.display = "block";
}
} catch (err) {
console.error("Failed to load OAuth providers:", err);
loadingEl.style.display = "none";
// Show all buttons as fallback (they'll redirect to error page if not configured)
buttonsEl.style.display = "grid";
buttonsEl.querySelectorAll("a").forEach((btn) => {
btn.classList.remove("hidden");
});
2025-11-29 16:29:28 -03:00
}
}
// Initialize
document.addEventListener("DOMContentLoaded", () => {
initTheme();
checkDevMode();
loadOAuthProviders();
// Handle form validation
const form = document.getElementById("login-form");
form.addEventListener("submit", (e) => {
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
if (!email || !password) {
e.preventDefault();
showError("Please fill in all fields");
return false;
}
});
2025-11-29 16:29:28 -03:00
});
// Show error message
function showError(message) {
const container = document.getElementById("message-container");
container.innerHTML = `<div class="error-message">${message}</div>`;
2025-11-29 16:29:28 -03:00
}
// Handle HTMX events
document.body.addEventListener("htmx:afterRequest", (event) => {
if (event.detail.xhr.status === 200) {
// Check if we got a redirect header
const redirect =
event.detail.xhr.getResponseHeader("HX-Redirect");
if (redirect) {
window.location.href = redirect;
}
}
});
document.body.addEventListener("htmx:responseError", (event) => {
showError(
"Authentication failed. Please check your credentials.",
);
});
</script>
</body>
2025-11-29 16:29:28 -03:00
</html>