botserver/ui/suite/auth/login.html
Rodrigo Rodriguez (Pragmatismo) e68a12176d Add Suite app documentation, templates, and Askama config
- Add askama.toml for template configuration (ui/ directory)
- Add Suite app documentation with flow diagrams (SVG)
  - App launcher, chat flow, drive flow, tasks flow
  - Individual app docs: chat, drive, tasks, mail, etc.
- Add HTML templates for Suite apps
  - Base template with header and app launcher
  - Auth login page
  - Chat, Drive, Mail, Meet, Tasks templates
  - Partial templates for messages, sessions, notifications
- Add Extensions type to AppState for type-erased storage
- Add mTLS module for service-to-service authentication
- Update web handlers to use new template paths (suite/)
- Fix auth module to avoid axum-extra TypedHeader dependency
2025-11-30 21:00:48 -03:00

351 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - General Bots</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--bg: #0f172a;
--surface: #1e293b;
--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;
}
.login-container {
width: 100%;
max-width: 400px;
padding: 2rem;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
width: 64px;
height: 64px;
margin: 0 auto 1rem;
background: var(--primary);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
.login-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: var(--text-secondary);
font-size: 0.875rem;
}
.login-form {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.form-input::placeholder {
color: var(--text-secondary);
}
.form-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
.form-checkbox input {
width: 16px;
height: 16px;
accent-color: var(--primary);
}
.login-btn {
width: 100%;
padding: 0.75rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
margin-top: 1rem;
}
.login-btn:hover {
background: var(--primary-hover);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-footer {
text-align: center;
margin-top: 1.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
.login-footer a {
color: var(--primary);
text-decoration: none;
}
.login-footer a:hover {
text-decoration: underline;
}
.error-message {
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--error);
color: var(--error);
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.875rem;
display: none;
}
.error-message.visible {
display: block;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.htmx-request .btn-text {
display: none;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.divider {
display: flex;
align-items: center;
margin: 1.5rem 0;
color: var(--text-secondary);
font-size: 0.75rem;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.divider span {
padding: 0 1rem;
}
.social-login {
display: flex;
gap: 0.75rem;
}
.social-btn {
flex: 1;
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 0.875rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: border-color 0.2s, background 0.2s;
}
.social-btn:hover {
border-color: var(--primary);
background: var(--surface);
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="login-logo">🤖</div>
<h1 class="login-title">Welcome Back</h1>
<p class="login-subtitle">Sign in to General Bots Suite</p>
</div>
<div class="login-form">
{% if let Some(error) = error %}
<div class="error-message visible">{{ error }}</div>
{% else %}
<div class="error-message" id="error-message"></div>
{% endif %}
<form hx-post="/api/auth/login"
hx-target="#error-message"
hx-swap="outerHTML"
hx-indicator=".login-btn">
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input type="email"
id="email"
name="email"
class="form-input"
placeholder="you@example.com"
required
autocomplete="email">
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input type="password"
id="password"
name="password"""
class="form-input"
placeholder="••••••••"
required
autocomplete="current-password">
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="remember" value="true">
<span>Remember me for 30 days</span>
</label>
</div>
<button type="submit" class="login-btn">
<span class="btn-text">Sign In</span>
<div class="spinner htmx-indicator"></div>
</button>
</form>
<div class="divider">
<span>or continue with</span>
</div>
<div class="social-login">
<button type="button" class="social-btn"
hx-get="/api/auth/oauth/google"
hx-swap="none">
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" 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"/>
<path fill="currentColor" 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"/>
<path fill="currentColor" 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"/>
<path fill="currentColor" 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"/>
</svg>
Google
</button>
<button type="button" class="social-btn"
hx-get="/api/auth/oauth/microsoft"
hx-swap="none">
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M11.4 24H0V12.6h11.4V24zM24 24H12.6V12.6H24V24zM11.4 11.4H0V0h11.4v11.4zm12.6 0H12.6V0H24v11.4z"/>
</svg>
Microsoft
</button>
</div>
</div>
<div class="login-footer">
<p>Don't have an account? <a href="/auth/register">Sign up</a></p>
<p style="margin-top: 0.5rem;"><a href="/auth/forgot-password">Forgot password?</a></p>
</div>
</div>
<script>
// Handle successful login redirect
document.body.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful && event.detail.xhr.status === 200) {
const response = event.detail.xhr.response;
if (response && response.includes('redirect')) {
window.location.href = '/';
}
}
});
</script>
</body>
</html>