Files

138 lines
6.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign In</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0f; color: #fff; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.card { width: 100%; max-width: 400px; padding: 2.5rem; border: 1px solid #222; border-radius: 12px; background: #111; }
h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: .5rem; }
.subtitle { color: #888; font-size: .875rem; margin-bottom: 1.5rem; }
.field { position: relative; margin-bottom: 1rem; }
.field input { width: 100%; padding: .75rem 1rem; background: #1a1a1f; border: 1px solid #333; border-radius: 8px; color: #fff; font-size: .875rem; outline: none; transition: border-color .2s; }
.field input:focus { border-color: #fd4444; }
.field input::placeholder { color: #666; }
.btn-primary { width: 100%; padding: .75rem; background: #fd4444; color: #fff; border: none; border-radius: 8px; font-size: .875rem; font-weight: 600; cursor: pointer; transition: opacity .2s; }
.btn-primary:hover { opacity: .9; }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.btn-sso { width: 100%; padding: .75rem; background: transparent; color: #fff; border: 1px solid #333; border-radius: 8px; font-size: .875rem; font-weight: 500; cursor: pointer; transition: border-color .2s; margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sso:hover { border-color: #666; }
.divider { display: flex; align-items: center; gap: 1rem; margin: 1.25rem 0; color: #555; font-size: .75rem; }
.divider::before, .divider::after { content: ''; flex: 1; border-top: 1px solid #333; }
.error { padding: .75rem; background: rgba(255,50,50,.1); border: 1px solid rgba(255,50,50,.2); border-radius: 8px; color: #f44; font-size: .8rem; margin-bottom: 1rem; display: none; }
.links { text-align: center; margin-top: 1rem; font-size: .8rem; color: #666; }
.links a { color: #fd4444; text-decoration: none; }
.links a:hover { text-decoration: underline; }
.spinner { display: none; width: 16px; height: 16px; border: 2px solid #fff3; border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
#app[data-loading="true"] .spinner { display: inline-block; }
#app[data-loading="true"] .btn-text { display: none; }
</style>
</head>
<body>
<div id="app" class="card">
<h1 id="title">Sign In</h1>
<p class="subtitle">Enter your credentials to continue</p>
<div id="error" class="error"></div>
<button class="btn-sso" onclick="startPKCE()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg>
Sign in with SSO
</button>
<div class="divider">or sign in with email</div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="field">
<input type="email" id="email" name="email" placeholder="Email address" autocomplete="email" required />
</div>
<div class="field">
<input type="password" id="password" name="password" placeholder="Password" autocomplete="current-password" required />
</div>
<button type="submit" class="btn-primary" id="submitBtn">
<span class="btn-text">Sign In</span>
<span class="spinner"></span>
</button>
</form>
<div class="links">
<a href="forgot-password.html">Forgot password?</a>
</div>
</div>
<script src="id.js"></script>
<script>
// Initialize from URL params or defaults
const params = new URLSearchParams(location.search)
const config = HanzoAuth.init({
issuer: params.get('issuer') || window.IAM_ISSUER || 'https://hanzo.id',
clientId: params.get('client_id')|| window.IAM_CLIENT_ID || 'app-hanzo',
redirectUri: params.get('redirect_uri') || location.origin + location.pathname,
})
// Handle callback (code in URL)
if (params.has('code')) {
HanzoAuth.handleCallback(params).then(tokens => {
if (tokens.error) {
showError(tokens.error_description || tokens.error)
} else {
// Store tokens and redirect
localStorage.setItem('hanzo_access_token', tokens.access_token)
if (tokens.refresh_token) localStorage.setItem('hanzo_refresh_token', tokens.refresh_token)
const dest = sessionStorage.getItem('hanzo_post_login') || '/'
sessionStorage.removeItem('hanzo_post_login')
location.href = dest
}
})
}
// Handle token in URL (from bridge)
if (params.has('access_token')) {
localStorage.setItem('hanzo_access_token', params.get('access_token'))
if (params.get('refresh_token')) localStorage.setItem('hanzo_refresh_token', params.get('refresh_token'))
const dest = sessionStorage.getItem('hanzo_post_login') || '/'
sessionStorage.removeItem('hanzo_post_login')
history.replaceState(null, '', location.pathname)
location.href = dest
}
function startPKCE() {
HanzoAuth.startPKCE()
}
async function handleLogin(e) {
e.preventDefault()
const app = document.getElementById('app')
const email = document.getElementById('email').value
const password = document.getElementById('password').value
app.dataset.loading = 'true'
hideError()
try {
const tokens = await HanzoAuth.passwordLogin(email, password)
if (tokens.error) throw new Error(tokens.error_description || tokens.error || 'Login failed')
localStorage.setItem('hanzo_access_token', tokens.access_token)
if (tokens.refresh_token) localStorage.setItem('hanzo_refresh_token', tokens.refresh_token)
location.href = '/'
} catch (err) {
showError(err.message)
} finally {
app.dataset.loading = 'false'
}
}
function showError(msg) {
const el = document.getElementById('error')
el.textContent = msg
el.style.display = 'block'
}
function hideError() {
document.getElementById('error').style.display = 'none'
}
</script>
</body>
</html>