502 lines
15 KiB
TypeScript
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,
|
|
}));
|
|
}
|
|
}
|