Files
kashilo/js/components/auth-modal.js
2026-01-31 14:18:57 +01:00

427 lines
12 KiB
JavaScript

/**
* Auth Modal - Login/Register with UUID
*/
import { t, i18n } from '../i18n.js';
import { auth } from '../services/auth.js';
class AuthModal extends HTMLElement {
constructor() {
super();
this.mode = 'login'; // 'login' | 'register' | 'show-uuid'
this.generatedUuid = null;
this.error = null;
this.loading = false;
}
connectedCallback() {
this.render();
this.unsubscribe = i18n.subscribe(() => this.render());
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe();
}
show(mode = 'login') {
this.mode = mode;
this.error = null;
this.generatedUuid = null;
this.hidden = false;
this.render();
document.body.style.overflow = 'hidden';
}
hide() {
this.hidden = true;
document.body.style.overflow = '';
this.dispatchEvent(new CustomEvent('close'));
}
switchMode(mode) {
this.mode = mode;
this.error = null;
this.render();
}
render() {
if (this.hidden) {
this.innerHTML = '';
return;
}
this.innerHTML = /* html */`
<div class="modal-overlay" id="modal-overlay">
<div class="modal-content">
<button class="modal-close" id="modal-close" aria-label="Close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
${this.mode === 'login' ? this.renderLogin() : ''}
${this.mode === 'register' ? this.renderRegister() : ''}
${this.mode === 'show-uuid' ? this.renderShowUuid() : ''}
</div>
</div>
`;
this.setupEventListeners();
}
renderLogin() {
const storedUuid = auth.getStoredUuid();
return /* html */`
<h2 class="modal-title">${t('auth.login')}</h2>
${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
<form id="login-form" class="auth-form">
<div class="form-group">
<label class="label" for="uuid">${t('auth.yourUuid')}</label>
<input
type="text"
class="input"
id="uuid"
name="uuid"
value="${storedUuid || ''}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
autocomplete="off"
spellcheck="false"
>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}>
${this.loading ? t('auth.loggingIn') : t('auth.login')}
</button>
</form>
<div class="auth-footer">
<p>${t('auth.noAccount')}
<button class="link-btn" id="switch-register">${t('auth.createAccount')}</button>
</p>
</div>
`;
}
renderRegister() {
return /* html */`
<h2 class="modal-title">${t('auth.createAccount')}</h2>
<div class="auth-info">
<p>${t('auth.registerInfo')}</p>
</div>
${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
<button
class="btn btn-primary btn-lg btn-block"
id="generate-uuid"
${this.loading ? 'disabled' : ''}
>
${this.loading ? t('auth.creating') : t('auth.generateUuid')}
</button>
<div class="auth-footer">
<p>${t('auth.hasAccount')}
<button class="link-btn" id="switch-login">${t('auth.login')}</button>
</p>
</div>
`;
}
renderShowUuid() {
return /* html */`
<h2 class="modal-title">${t('auth.accountCreated')}</h2>
<div class="auth-warning">
<strong>${t('auth.important')}</strong>
<p>${t('auth.saveUuidWarning')}</p>
</div>
<div class="uuid-display">
<code id="uuid-value">${this.generatedUuid}</code>
<button class="btn btn-outline" id="copy-uuid" title="${t('auth.copy')}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div class="uuid-actions">
<button class="btn btn-outline btn-lg" id="download-uuid">
${t('auth.downloadBackup')}
</button>
</div>
<button class="btn btn-primary btn-lg btn-block" id="confirm-saved">
${t('auth.confirmSaved')}
</button>
`;
}
setupEventListeners() {
// Close modal
this.querySelector('#modal-overlay')?.addEventListener('click', (e) => {
if (e.target.id === 'modal-overlay') this.hide();
});
this.querySelector('#modal-close')?.addEventListener('click', () => this.hide());
// Switch modes
this.querySelector('#switch-register')?.addEventListener('click', () => this.switchMode('register'));
this.querySelector('#switch-login')?.addEventListener('click', () => this.switchMode('login'));
// Login form
this.querySelector('#login-form')?.addEventListener('submit', (e) => this.handleLogin(e));
// Generate UUID
this.querySelector('#generate-uuid')?.addEventListener('click', () => this.handleRegister());
// Copy UUID
this.querySelector('#copy-uuid')?.addEventListener('click', () => this.copyUuid());
// Download backup
this.querySelector('#download-uuid')?.addEventListener('click', () => this.downloadBackup());
// Confirm saved
this.querySelector('#confirm-saved')?.addEventListener('click', () => this.hide());
// Escape key
document.addEventListener('keydown', this.handleKeydown.bind(this));
}
handleKeydown(e) {
if (e.key === 'Escape' && !this.hidden) {
this.hide();
}
}
async handleLogin(e) {
e.preventDefault();
const uuid = this.querySelector('#uuid').value.trim();
if (!uuid) {
this.error = t('auth.enterUuid');
this.render();
return;
}
this.loading = true;
this.error = null;
this.render();
const result = await auth.login(uuid);
this.loading = false;
if (result.success) {
this.hide();
this.dispatchEvent(new CustomEvent('login', { detail: { success: true } }));
} else {
this.error = result.error || t('auth.invalidUuid');
this.render();
}
}
async handleRegister() {
this.loading = true;
this.error = null;
this.render();
const result = await auth.createAccount();
this.loading = false;
if (result.success) {
this.generatedUuid = result.uuid;
this.mode = 'show-uuid';
this.render();
this.dispatchEvent(new CustomEvent('register', { detail: { uuid: result.uuid } }));
} else {
this.error = result.error || t('auth.registrationFailed');
this.render();
}
}
async copyUuid() {
try {
await navigator.clipboard.writeText(this.generatedUuid);
const btn = this.querySelector('#copy-uuid');
btn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
setTimeout(() => this.render(), 2000);
} catch (e) {
console.error('Copy failed:', e);
}
}
downloadBackup() {
const content = `dgray.io Account Backup
========================
Your UUID (keep this secret!):
${this.generatedUuid}
Login URL:
https://dgray.io/#/login
Created: ${new Date().toISOString()}
WARNING: If you lose this UUID, you cannot recover your account!
`;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dgray-backup-${this.generatedUuid.slice(0, 8)}.txt`;
a.click();
URL.revokeObjectURL(url);
}
}
customElements.define('auth-modal', AuthModal);
const style = document.createElement('style');
style.textContent = /* css */`
auth-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: var(--z-modal);
}
auth-modal[hidden] {
display: none;
}
auth-modal .modal-overlay {
position: absolute;
inset: 0;
background: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-md);
}
auth-modal .modal-content {
background: var(--color-bg);
border-radius: var(--radius-lg);
padding: var(--space-xl);
width: 100%;
max-width: 420px;
position: relative;
box-shadow: var(--shadow-xl);
}
auth-modal .modal-close {
position: absolute;
top: var(--space-md);
right: var(--space-md);
background: none;
border: none;
cursor: pointer;
color: var(--color-text-muted);
padding: var(--space-xs);
}
auth-modal .modal-close:hover {
color: var(--color-text);
}
auth-modal .modal-title {
margin: 0 0 var(--space-lg);
font-size: var(--font-size-2xl);
}
auth-modal .auth-form .form-group {
margin-bottom: var(--space-lg);
}
auth-modal .auth-info {
background: var(--color-bg-secondary);
border-radius: var(--radius-md);
padding: var(--space-md);
margin-bottom: var(--space-lg);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
}
auth-modal .auth-warning {
background: var(--color-bg-tertiary);
border-left: 4px solid var(--color-accent);
border-radius: var(--radius-md);
padding: var(--space-md);
margin-bottom: var(--space-lg);
}
auth-modal .auth-warning strong {
display: block;
margin-bottom: var(--space-xs);
color: var(--color-accent);
}
auth-modal .auth-error {
background: var(--color-bg-tertiary);
border-left: 4px solid var(--color-error);
border-radius: var(--radius-md);
padding: var(--space-md);
margin-bottom: var(--space-lg);
color: var(--color-text);
}
auth-modal .uuid-display {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
}
auth-modal .uuid-display code {
flex: 1;
background: var(--color-bg-secondary);
padding: var(--space-md);
border-radius: var(--radius-md);
font-family: monospace;
font-size: var(--font-size-sm);
word-break: break-all;
border: 1px solid var(--color-border);
}
auth-modal .uuid-actions {
margin-bottom: var(--space-lg);
}
auth-modal .btn-block {
width: 100%;
}
auth-modal .auth-footer {
margin-top: var(--space-lg);
text-align: center;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
auth-modal .link-btn {
background: none;
border: none;
color: var(--color-accent);
cursor: pointer;
font-size: inherit;
text-decoration: underline;
}
auth-modal .link-btn:hover {
color: var(--color-accent-hover);
}
`;
document.head.appendChild(style);
export default AuthModal;