471 lines
14 KiB
TypeScript
471 lines
14 KiB
TypeScript
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
css,
|
|
cssManager,
|
|
property,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
import type { ISgUser, ISgSession } from '../interfaces.js';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'sg-settings-view': SgSettingsView;
|
|
}
|
|
}
|
|
|
|
@customElement('sg-settings-view')
|
|
export class SgSettingsView extends DeesElement {
|
|
public static demo = () => html`
|
|
<div style="padding: 24px; max-width: 900px; background: #09090b;">
|
|
<sg-settings-view
|
|
.user=${{
|
|
id: 'u1',
|
|
email: 'admin@stack.gallery',
|
|
username: 'admin',
|
|
displayName: 'Admin User',
|
|
avatarUrl: '',
|
|
isSystemAdmin: true,
|
|
}}
|
|
.sessions=${[
|
|
{ id: 's1', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0', ipAddress: '192.168.1.100', isValid: true, lastActivityAt: '2026-03-20T10:30:00Z', createdAt: '2026-03-18T08:00:00Z' },
|
|
{ id: 's2', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/17.0', ipAddress: '10.0.0.42', isValid: true, lastActivityAt: '2026-03-19T15:45:00Z', createdAt: '2026-03-15T14:00:00Z' },
|
|
{ id: 's3', userAgent: 'curl/8.4.0', ipAddress: '203.0.113.50', isValid: false, lastActivityAt: '2026-03-10T22:00:00Z', createdAt: '2026-03-10T20:00:00Z' },
|
|
]}
|
|
></sg-settings-view>
|
|
</div>
|
|
`;
|
|
|
|
public static demoGroups = ['Auth'];
|
|
|
|
@property({ type: Object })
|
|
public accessor user: ISgUser = {
|
|
id: '',
|
|
email: '',
|
|
username: '',
|
|
displayName: '',
|
|
isSystemAdmin: false,
|
|
};
|
|
|
|
@property({ type: Array })
|
|
public accessor sessions: ISgSession[] = [];
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
|
}
|
|
|
|
.container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 32px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* Section */
|
|
.section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.section-box {
|
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.section-subtitle {
|
|
font-size: 13px;
|
|
color: ${cssManager.bdTheme('#888', '#777')};
|
|
margin-top: -8px;
|
|
}
|
|
|
|
/* Form elements */
|
|
.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 {
|
|
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;
|
|
font-family: inherit;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
|
}
|
|
|
|
.form-input:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 12px;
|
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
|
}
|
|
|
|
.save-btn {
|
|
align-self: flex-start;
|
|
padding: 8px 20px;
|
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
|
border: none;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
|
cursor: pointer;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
|
|
.save-btn:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* Admin badge */
|
|
.admin-badge {
|
|
display: inline-flex;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: 2px 8px;
|
|
background: rgba(59, 130, 246, 0.15);
|
|
color: #3b82f6;
|
|
}
|
|
|
|
/* Sessions */
|
|
.session-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
|
}
|
|
|
|
.session-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
|
}
|
|
|
|
.session-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.session-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.session-agent {
|
|
font-size: 13px;
|
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
|
font-family: 'JetBrains Mono', monospace;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 500px;
|
|
}
|
|
|
|
.session-meta {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 12px;
|
|
color: ${cssManager.bdTheme('#888', '#777')};
|
|
}
|
|
|
|
.session-status {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
padding: 1px 6px;
|
|
}
|
|
|
|
.session-status.active {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.session-status.expired {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.session-actions {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.revoke-session-btn {
|
|
padding: 4px 12px;
|
|
background: transparent;
|
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
|
font-size: 12px;
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
.revoke-session-btn:hover {
|
|
border-color: #ef4444;
|
|
color: #ef4444;
|
|
}
|
|
|
|
/* Danger zone */
|
|
.danger-section {
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
.danger-title {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.danger-text {
|
|
font-size: 13px;
|
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.danger-btn {
|
|
align-self: flex-start;
|
|
padding: 8px 16px;
|
|
background: transparent;
|
|
border: 1px solid #ef4444;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #ef4444;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
.danger-btn:hover {
|
|
background: #ef4444;
|
|
color: #fff;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="container">
|
|
<div class="page-title">Settings</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">
|
|
Profile
|
|
${this.user.isSystemAdmin ? html`<span class="admin-badge">Admin</span>` : ''}
|
|
</div>
|
|
<div class="section-box">
|
|
<div class="form-group">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-input" .value=${this.user.email} disabled>
|
|
<span class="form-hint">Email cannot be changed</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" class="form-input" .value=${this.user.username} disabled>
|
|
<span class="form-hint">Username cannot be changed</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Display Name</label>
|
|
<input
|
|
type="text"
|
|
id="settings-displayname"
|
|
class="form-input"
|
|
.value=${this.user.displayName}
|
|
placeholder="Your display name"
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Avatar URL</label>
|
|
<input
|
|
type="url"
|
|
id="settings-avatar"
|
|
class="form-input"
|
|
.value=${this.user.avatarUrl || ''}
|
|
placeholder="https://..."
|
|
>
|
|
</div>
|
|
<button class="save-btn" @click=${this.handleSaveProfile}>Save Profile</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Change Password</div>
|
|
<div class="section-box">
|
|
<div class="form-group">
|
|
<label class="form-label">Current Password</label>
|
|
<input type="password" id="settings-current-pw" class="form-input" placeholder="Current password">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">New Password</label>
|
|
<input type="password" id="settings-new-pw" class="form-input" placeholder="New password">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Confirm New Password</label>
|
|
<input type="password" id="settings-confirm-pw" class="form-input" placeholder="Confirm new password">
|
|
</div>
|
|
<button class="save-btn" @click=${this.handleChangePassword}>Update Password</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Active Sessions</div>
|
|
${this.sessions.length > 0
|
|
? html`
|
|
<div class="session-list">
|
|
${this.sessions.map(
|
|
(session) => html`
|
|
<div class="session-row">
|
|
<div class="session-info">
|
|
<div class="session-agent">${this.parseUserAgent(session.userAgent || 'Unknown client')}</div>
|
|
<div class="session-meta">
|
|
<span>${session.ipAddress || 'unknown'}</span>
|
|
<span>Active ${this.formatDate(session.lastActivityAt || '')}</span>
|
|
<span class="session-status ${session.isValid ? 'active' : 'expired'}">
|
|
${session.isValid ? 'Active' : 'Expired'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="session-actions">
|
|
${session.isValid
|
|
? html`
|
|
<button
|
|
class="revoke-session-btn"
|
|
@click=${() => this.handleRevokeSession(session.id)}
|
|
>Revoke</button>
|
|
`
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
`
|
|
)}
|
|
</div>
|
|
`
|
|
: html`<div class="section-box">No active sessions</div>`}
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title danger-title">Danger Zone</div>
|
|
<div class="section-box danger-section">
|
|
<div class="danger-text">
|
|
Permanently delete your account and all associated data. This action cannot be undone.
|
|
All your tokens will be revoked and organization memberships removed.
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Confirm with password</label>
|
|
<input type="password" id="settings-delete-pw" class="form-input" placeholder="Enter your password">
|
|
</div>
|
|
<button class="danger-btn" @click=${this.handleDeleteAccount}>Delete My Account</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private parseUserAgent(ua: string): string {
|
|
if (!ua) return 'Unknown client';
|
|
if (ua.length > 80) return ua.substring(0, 77) + '...';
|
|
return ua;
|
|
}
|
|
|
|
private formatDate(dateStr: string): string {
|
|
if (!dateStr) return '';
|
|
try {
|
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
private handleSaveProfile() {
|
|
const displayName = (this.shadowRoot?.getElementById('settings-displayname') as HTMLInputElement)?.value || '';
|
|
const avatarUrl = (this.shadowRoot?.getElementById('settings-avatar') as HTMLInputElement)?.value || '';
|
|
this.dispatchEvent(
|
|
new CustomEvent('save-profile', {
|
|
detail: { displayName, avatarUrl },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
private handleChangePassword() {
|
|
const currentPassword = (this.shadowRoot?.getElementById('settings-current-pw') as HTMLInputElement)?.value || '';
|
|
const newPassword = (this.shadowRoot?.getElementById('settings-new-pw') as HTMLInputElement)?.value || '';
|
|
const confirmPassword = (this.shadowRoot?.getElementById('settings-confirm-pw') as HTMLInputElement)?.value || '';
|
|
if (!currentPassword || !newPassword) return;
|
|
if (newPassword !== confirmPassword) return;
|
|
this.dispatchEvent(
|
|
new CustomEvent('change-password', {
|
|
detail: { currentPassword, newPassword },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
private handleRevokeSession(sessionId: string) {
|
|
this.dispatchEvent(
|
|
new CustomEvent('revoke-session', {
|
|
detail: { sessionId },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
private handleDeleteAccount() {
|
|
const password = (this.shadowRoot?.getElementById('settings-delete-pw') as HTMLInputElement)?.value || '';
|
|
if (!password) return;
|
|
this.dispatchEvent(
|
|
new CustomEvent('delete-account', {
|
|
detail: { password },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
}
|