- sidebar: fix library/tag selection event handlers not firing (callbacks never invoked) - sidebar: fix handleSelectLibrary always passing empty string instead of library id - dashboard: fix tag filter not persisting when navigating from document view - app: fix XSS vulnerability in showToast (API error messages not escaped) - app: fix XSS vulnerability in confirmDelete modal message - document: fix path traversal risk in export filename
149 lines
3.8 KiB
JavaScript
149 lines
3.8 KiB
JavaScript
// SimpleNote Web - Main Application
|
|
|
|
import { api } from './api.js';
|
|
import { renderLogin, initLoginHandlers } from './views/login.js';
|
|
import { renderDashboard } from './views/dashboard.js';
|
|
import { renderDocument } from './views/document.js';
|
|
import { renderEditor } from './views/editor.js';
|
|
|
|
class App {
|
|
constructor() {
|
|
this.currentView = null;
|
|
this.state = {
|
|
token: localStorage.getItem('sn_token'),
|
|
view: 'dashboard',
|
|
params: {}
|
|
};
|
|
}
|
|
|
|
async init() {
|
|
if (!this.state.token) {
|
|
this.renderLogin();
|
|
return;
|
|
}
|
|
|
|
api.setToken(this.state.token);
|
|
|
|
try {
|
|
await api.login(this.state.token);
|
|
this.render();
|
|
} catch (e) {
|
|
this.state.token = null;
|
|
localStorage.removeItem('sn_token');
|
|
this.renderLogin();
|
|
}
|
|
}
|
|
|
|
renderLogin() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = renderLogin();
|
|
initLoginHandlers(async (token) => {
|
|
try {
|
|
await api.login(token);
|
|
this.state.token = token;
|
|
this.state.view = 'dashboard';
|
|
this.render();
|
|
} catch (e) {
|
|
return 'Invalid token';
|
|
}
|
|
});
|
|
}
|
|
|
|
async render() {
|
|
const app = document.getElementById('app');
|
|
|
|
switch (this.state.view) {
|
|
case 'dashboard':
|
|
await renderDashboard(this);
|
|
break;
|
|
case 'document':
|
|
await renderDocument(this);
|
|
break;
|
|
case 'editor':
|
|
renderEditor(this);
|
|
break;
|
|
default:
|
|
await renderDashboard(this);
|
|
}
|
|
}
|
|
|
|
navigate(view, params = {}) {
|
|
this.state.view = view;
|
|
this.state.params = params;
|
|
this.render();
|
|
}
|
|
|
|
showToast(message, type = 'info') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
const escapedMessage = this.escapeHtml(message);
|
|
toast.innerHTML = `
|
|
<span class="toast-message">${escapedMessage}</span>
|
|
<button class="toast-close" onclick="this.parentElement.remove()">✕</button>
|
|
`;
|
|
container.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 4000);
|
|
}
|
|
|
|
escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async confirmDelete(message) {
|
|
return new Promise((resolve) => {
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'modal-backdrop';
|
|
const escapedMessage = this.escapeHtml(message);
|
|
backdrop.innerHTML = `
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<span>⚠️</span>
|
|
<h3>Confirm Delete</h3>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>${escapedMessage}</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-ghost" id="cancel-btn">Cancel</button>
|
|
<button class="btn btn-danger" id="confirm-btn">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(backdrop);
|
|
|
|
backdrop.querySelector('#cancel-btn').onclick = () => {
|
|
backdrop.remove();
|
|
resolve(false);
|
|
};
|
|
backdrop.querySelector('#confirm-btn').onclick = () => {
|
|
backdrop.remove();
|
|
resolve(true);
|
|
};
|
|
backdrop.onclick = (e) => {
|
|
if (e.target === backdrop) {
|
|
backdrop.remove();
|
|
resolve(false);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
window.app = new App();
|
|
app.init();
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && window.app.state.view === 'editor') {
|
|
window.app.navigate('dashboard');
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n' && window.app.state.view === 'dashboard') {
|
|
e.preventDefault();
|
|
window.app.navigate('editor');
|
|
}
|
|
});
|