- Create core/oauth module with OAuthProvider enum and shared types
- Implement providers.rs with auth URLs, token exchange, user info endpoints
- Add routes for /auth/oauth/providers, /auth/oauth/{provider}, and callbacks
- Update login.html with OAuth button grid and dynamic provider loading
- Add OAuth config settings to config.csv with setup documentation and links
- Uses HTMX for login form, minimal JS for OAuth provider visibility
730 lines
28 KiB
HTML
730 lines
28 KiB
HTML
<!doctype html>
|
|
<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;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--background: #0f172a;
|
|
--surface: #1e293b;
|
|
--text: #f1f5f9;
|
|
--text-secondary: #94a3b8;
|
|
--border: #334155;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
.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>
|
|
</div>
|
|
|
|
<h1>Sign in to your account</h1>
|
|
|
|
<!-- Error Message -->
|
|
{% if error_message %}
|
|
<div class="error-message">{{ error_message }}</div>
|
|
{% endif %}
|
|
|
|
<!-- 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>
|
|
|
|
<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>
|
|
</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");
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Show error message
|
|
function showError(message) {
|
|
const container = document.getElementById("message-container");
|
|
container.innerHTML = `<div class="error-message">${message}</div>`;
|
|
}
|
|
|
|
// 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>
|
|
</html>
|