/**
* Login Page for better-auth Social Sign-in
*
* Displays available social login providers based on environment configuration.
* Uses POST forms (required by better-auth) instead of GET links.
* Supports callbackURL parameter for redirect after login (used by MCP OAuth flow).
*/
import type { Env } from '../types';
/**
* Generate login page HTML with configured social providers
* @param env - Environment bindings
* @param callbackURL - Optional URL to redirect to after successful login
*/
export function getLoginPage(env: Env, callbackURL?: string): string {
// Determine available providers based on configured credentials
const providers: Array<{ id: string; name: string; color: string; hoverColor: string; icon: string }> = [];
if (env.GOOGLE_CLIENT_ID) {
providers.push({
id: 'google',
name: 'Google',
color: '#4285f4',
hoverColor: '#3367d6',
icon: `<svg viewBox="0 0 24 24" width="20" height="20" 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"/><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"/><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"/><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"/></svg>`,
});
}
if (env.MICROSOFT_CLIENT_ID) {
providers.push({
id: 'microsoft',
name: 'Microsoft',
color: '#00a4ef',
hoverColor: '#0078d4',
icon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M11.4 24H0V12.6h11.4V24zM24 24H12.6V12.6H24V24zM11.4 11.4H0V0h11.4v11.4zm12.6 0H12.6V0H24v11.4z"/></svg>`,
});
}
if (env.GITHUB_CLIENT_ID) {
providers.push({
id: 'github',
name: 'GitHub',
color: '#24292e',
hoverColor: '#1b1f23',
icon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
});
}
// Default callback URL (dashboard or passed callbackURL)
const redirectURL = callbackURL || '/dashboard';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In - MCP Server</title>
<link rel="icon" href="https://www.jezweb.com.au/wp-content/uploads/2020/03/favicon-100x100.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #09090b;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
position: relative;
overflow: hidden;
}
/* Background effects */
body::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(ellipse at 20% 20%, rgba(20, 184, 166, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
pointer-events: none;
}
/* Grid overlay */
body::after {
content: '';
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 64px 64px;
pointer-events: none;
}
.card {
background: rgba(24, 24, 27, 0.95);
border: 1px solid rgba(63, 63, 70, 0.5);
border-radius: 16px;
padding: 48px 40px;
max-width: 420px;
width: 100%;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
position: relative;
z-index: 1;
}
.logo {
width: 56px;
height: 56px;
margin: 0 auto 24px;
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.logo svg {
width: 28px;
height: 28px;
fill: white;
}
h1 {
color: #fafafa;
font-size: 1.75rem;
font-weight: 700;
text-align: center;
margin-bottom: 8px;
}
.subtitle {
color: #a1a1aa;
text-align: center;
margin-bottom: 32px;
font-size: 0.95rem;
line-height: 1.5;
}
.providers {
display: flex;
flex-direction: column;
gap: 12px;
}
.provider-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 20px;
border-radius: 10px;
border: none;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
color: white;
width: 100%;
font-family: inherit;
}
.provider-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.4);
}
.provider-btn:active {
transform: translateY(0);
}
.provider-btn svg {
flex-shrink: 0;
}
.provider-btn.loading {
opacity: 0.7;
pointer-events: none;
}
.provider-btn .btn-text { display: inline; }
.provider-btn .btn-loading { display: none; }
.provider-btn.loading .btn-text { display: none; }
.provider-btn.loading .btn-loading { display: inline; }
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.security-note {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 14px;
background: rgba(20, 184, 166, 0.08);
border: 1px solid rgba(20, 184, 166, 0.2);
border-radius: 8px;
margin-top: 24px;
}
.security-note svg {
width: 18px;
height: 18px;
color: #14b8a6;
flex-shrink: 0;
margin-top: 1px;
}
.security-note p {
color: #a1a1aa;
font-size: 0.8rem;
line-height: 1.5;
}
.footer {
text-align: center;
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid #27272a;
color: #71717a;
font-size: 0.8rem;
}
.footer a {
color: #14b8a6;
text-decoration: none;
transition: color 0.2s;
}
.footer a:hover {
color: #2dd4bf;
text-decoration: underline;
}
.no-providers {
color: #f87171;
text-align: center;
padding: 24px;
background: rgba(248, 113, 113, 0.1);
border-radius: 10px;
border: 1px solid rgba(248, 113, 113, 0.2);
font-size: 0.9rem;
}
@media (max-width: 480px) {
.card {
padding: 32px 24px;
}
h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="card">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
</div>
<h1>Sign In</h1>
<p class="subtitle">Connect your account to access MCP tools</p>
<div class="providers">
${providers.length > 0
? providers.map(p => `
<form action="/api/auth/sign-in/social" method="POST" class="provider-form">
<input type="hidden" name="provider" value="${p.id}">
<input type="hidden" name="callbackURL" value="${escapeHtml(redirectURL)}">
<button type="submit" class="provider-btn" style="background: ${p.color};"
onmouseover="this.style.background='${p.hoverColor}'"
onmouseout="this.style.background='${p.color}'">
<span class="btn-icon">${p.icon}</span>
<span class="btn-text">Continue with ${p.name}</span>
<span class="btn-loading">
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"/>
</svg>
Redirecting...
</span>
</button>
</form>
`).join('')
: '<div class="no-providers">No authentication providers configured. Contact the administrator.</div>'
}
</div>
<div class="security-note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<path d="M9 12l2 2 4-4"/>
</svg>
<p>Your credentials are handled securely by your identity provider. We never see your password.</p>
</div>
<div class="footer">
<p>Powered by <a href="https://jezweb.com.au" target="_blank">Jezweb</a></p>
</div>
</div>
<script>
// Handle form submission - convert to JSON and fetch (better-auth requires JSON)
document.querySelectorAll('.provider-form').forEach(function(form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
var btn = this.querySelector('.provider-btn');
btn.classList.add('loading');
var formData = new FormData(this);
var provider = formData.get('provider');
var callbackURL = formData.get('callbackURL');
try {
var response = await fetch('/api/auth/sign-in/social', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: provider, callbackURL: callbackURL }),
credentials: 'include'
});
var data = await response.json();
if (data.url) {
window.location.href = data.url;
} else if (data.error) {
btn.classList.remove('loading');
alert('Sign in failed: ' + (data.message || data.error));
}
} catch (error) {
btn.classList.remove('loading');
alert('Sign in failed. Please try again.');
console.error('Sign in error:', error);
}
});
});
</script>
</body>
</html>`;
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}