Initial commit: scaffold stack.gallery catalog frontend
This commit is contained in:
501
ts_web/elements/sg-login-view.ts
Normal file
501
ts_web/elements/sg-login-view.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user