Files
catalog/ts_web/elements/sg-login-view.ts

502 lines
15 KiB
TypeScript

import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import type { ISgAuthProvider } from '../interfaces.js';
declare global {
interface HTMLElementTagNameMap {
'sg-login-view': SgLoginView;
}
}
@customElement('sg-login-view')
export class SgLoginView extends DeesElement {
public static demo = () => html`
<div style="height: 700px; display: flex; align-items: center; justify-content: center; background: #09090b;">
<sg-login-view
.providers=${[
{ id: 'github', name: 'github', displayName: 'GitHub', type: 'oidc' as const },
{ id: 'gitlab', name: 'gitlab', displayName: 'GitLab', type: 'oidc' as const },
{ id: 'corp-ldap', name: 'corp-ldap', displayName: 'Corporate LDAP', type: 'ldap' as const },
]}
.localAuthEnabled=${true}
.error=${''}
></sg-login-view>
</div>
`;
public static demoGroups = ['Auth'];
@property({ type: Array })
public accessor providers: ISgAuthProvider[] = [];
@property({ type: Boolean })
public accessor localAuthEnabled: boolean = true;
@property({ type: Boolean })
public accessor loading: boolean = false;
@property({ type: String })
public accessor error: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
}
.login-container {
width: 100%;
max-width: 420px;
padding: 24px;
}
.login-card {
background: ${cssManager.bdTheme('#ffffff', '#111')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
padding: 40px 32px;
}
.logo-section {
text-align: center;
margin-bottom: 32px;
}
.logo {
width: 56px;
height: 56px;
background: ${cssManager.bdTheme('#111', '#fff')};
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.logo svg {
width: 32px;
height: 32px;
color: ${cssManager.bdTheme('#fff', '#111')};
}
.brand-title {
font-size: 22px;
font-weight: 700;
color: ${cssManager.bdTheme('#111', '#fff')};
margin-bottom: 4px;
letter-spacing: -0.02em;
}
.brand-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#111', '#ddd')};
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-input {
width: 100%;
padding: 10px 12px;
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 14px;
color: ${cssManager.bdTheme('#111', '#fff')};
outline: none;
transition: border-color 150ms ease;
box-sizing: border-box;
font-family: inherit;
}
.form-input:focus {
border-color: ${cssManager.bdTheme('#111', '#fff')};
}
.form-input::placeholder {
color: ${cssManager.bdTheme('#aaa', '#555')};
}
.form-input.has-error {
border-color: #ef4444;
}
.error-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
font-size: 13px;
color: #f87171;
}
.submit-btn {
width: 100%;
padding: 10px 20px;
background: ${cssManager.bdTheme('#111', '#fff')};
border: none;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#fff', '#111')};
cursor: pointer;
transition: opacity 150ms ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.submit-btn:hover:not(:disabled) {
opacity: 0.85;
}
.submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.divider {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.divider-line {
flex: 1;
height: 1px;
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.divider-text {
font-size: 12px;
color: ${cssManager.bdTheme('#999', '#666')};
text-transform: uppercase;
letter-spacing: 0.05em;
}
.oauth-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.oauth-btn {
width: 100%;
padding: 10px 16px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#333', '#ddd')};
cursor: pointer;
transition: all 150ms ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.oauth-btn:hover {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
border-color: ${cssManager.bdTheme('#ccc', '#555')};
}
.ldap-section {
margin-top: 8px;
}
.ldap-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
font-size: 13px;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
border: none;
background: transparent;
width: 100%;
text-align: left;
}
.ldap-toggle:hover {
color: ${cssManager.bdTheme('#333', '#ddd')};
}
.ldap-form {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.ldap-provider-select {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.ldap-provider-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
transition: all 150ms ease;
}
.ldap-provider-btn:hover,
.ldap-provider-btn.active {
background: ${cssManager.bdTheme('#111', '#fff')};
color: ${cssManager.bdTheme('#fff', '#111')};
border-color: ${cssManager.bdTheme('#111', '#fff')};
}
.footer {
margin-top: 24px;
text-align: center;
font-size: 12px;
color: ${cssManager.bdTheme('#999', '#555')};
}
`,
];
private ldapExpanded = false;
private selectedLdapProviderId = '';
private get oauthProviders(): ISgAuthProvider[] {
return this.providers.filter((p) => p.type === 'oidc');
}
private get ldapProviders(): ISgAuthProvider[] {
return this.providers.filter((p) => p.type === 'ldap');
}
public render(): TemplateResult {
const hasOauth = this.oauthProviders.length > 0;
const hasLdap = this.ldapProviders.length > 0;
return html`
<div class="login-container">
<div class="login-card">
<div class="logo-section">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
<div class="brand-title">Stack.Gallery</div>
<div class="brand-subtitle">Sign in to your registry</div>
</div>
${this.error ? html`
<div class="error-banner">${this.error}</div>
` : ''}
${this.localAuthEnabled ? html`
<form class="form" @submit=${this.handleLocalLogin}>
<div class="form-group">
<label class="form-label">Email</label>
<input
type="email"
id="login-email"
class="form-input ${this.error ? 'has-error' : ''}"
placeholder="you@example.com"
autocomplete="email"
?disabled=${this.loading}
required
>
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input
type="password"
id="login-password"
class="form-input ${this.error ? 'has-error' : ''}"
placeholder="Enter your password"
autocomplete="current-password"
?disabled=${this.loading}
required
>
</div>
<button type="submit" class="submit-btn" ?disabled=${this.loading}>
${this.loading ? html`<div class="spinner"></div> Signing in...` : 'Sign in'}
</button>
</form>
` : ''}
${hasOauth ? html`
${this.localAuthEnabled ? html`
<div class="divider">
<span class="divider-line"></span>
<span class="divider-text">or continue with</span>
<span class="divider-line"></span>
</div>
` : ''}
<div class="oauth-buttons">
${this.oauthProviders.map(
(p) => html`
<button
class="oauth-btn"
@click=${() => this.handleOAuthLogin(p.id)}
?disabled=${this.loading}
>
${p.displayName}
</button>
`
)}
</div>
` : ''}
${hasLdap ? html`
<div class="ldap-section">
${this.localAuthEnabled || hasOauth ? html`
<div class="divider">
<span class="divider-line"></span>
<span class="divider-text">enterprise</span>
<span class="divider-line"></span>
</div>
` : ''}
<button class="ldap-toggle" @click=${() => { this.ldapExpanded = !this.ldapExpanded; this.requestUpdate(); }}>
${this.ldapExpanded ? '\u25BE' : '\u25B8'} Sign in with LDAP
</button>
${this.ldapExpanded ? html`
<div class="ldap-form">
${this.ldapProviders.length > 1 ? html`
<div class="ldap-provider-select">
${this.ldapProviders.map(
(p) => html`
<button
class="ldap-provider-btn ${this.selectedLdapProviderId === p.id ? 'active' : ''}"
@click=${() => { this.selectedLdapProviderId = p.id; this.requestUpdate(); }}
>${p.displayName}</button>
`
)}
</div>
` : ''}
<div class="form-group">
<label class="form-label">Username</label>
<input
type="text"
id="ldap-username"
class="form-input"
placeholder="LDAP username"
autocomplete="username"
?disabled=${this.loading}
required
>
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input
type="password"
id="ldap-password"
class="form-input"
placeholder="LDAP password"
autocomplete="current-password"
?disabled=${this.loading}
required
>
</div>
<button class="submit-btn" @click=${this.handleLdapLogin} ?disabled=${this.loading}>
${this.loading ? html`<div class="spinner"></div> Authenticating...` : 'Sign in with LDAP'}
</button>
</div>
` : ''}
</div>
` : ''}
<div class="footer">
Powered by Stack.Gallery
</div>
</div>
</div>
`;
}
private handleLocalLogin(e: Event) {
e.preventDefault();
const emailInput = this.shadowRoot?.getElementById('login-email') as HTMLInputElement;
const passwordInput = this.shadowRoot?.getElementById('login-password') as HTMLInputElement;
if (!emailInput || !passwordInput) return;
const email = emailInput.value.trim();
const password = passwordInput.value;
if (!email || !password) return;
this.dispatchEvent(new CustomEvent('login', {
detail: { email, password },
bubbles: true,
composed: true,
}));
}
private handleOAuthLogin(providerId: string) {
this.dispatchEvent(new CustomEvent('oauth-login', {
detail: { providerId },
bubbles: true,
composed: true,
}));
}
private handleLdapLogin() {
const usernameInput = this.shadowRoot?.getElementById('ldap-username') as HTMLInputElement;
const passwordInput = this.shadowRoot?.getElementById('ldap-password') as HTMLInputElement;
if (!usernameInput || !passwordInput) return;
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) return;
const providerId = this.selectedLdapProviderId || this.ldapProviders[0]?.id;
if (!providerId) return;
this.dispatchEvent(new CustomEvent('ldap-login', {
detail: { providerId, username, password },
bubbles: true,
composed: true,
}));
}
}