first attempt to get unified item creation menu

This commit is contained in:
Gamosoft 2025-11-26 12:43:37 +01:00
parent 8d78c278ee
commit 01ea9f6008
2 changed files with 30 additions and 62 deletions

View File

@ -429,41 +429,5 @@ All endpoints return JSON responses:
``` ```
--- ---
---
## 🔒 Security & Rate Limiting
### Authentication
When authentication is enabled (via `AUTHENTICATION_ENABLED` environment variable or `config.yaml`), all API endpoints require a valid session cookie obtained through the `/login` endpoint.
### Rate Limiting
When `DEMO_MODE` is enabled, the following rate limits are enforced:
**Read Operations:**
- `GET /api/notes` - 120/minute
- `GET /api/templates` - 120/minute
- `GET /api/templates/{template_name}` - 120/minute
- `GET /api/tags` - 120/minute
- `GET /api/tags/{tag}` - 120/minute
**Write Operations:**
- `POST /api/notes` - 60/minute
- `DELETE /api/notes/*` - 60/minute
- `POST /api/folders` - 60/minute
- `DELETE /api/folders/*` - 60/minute
- `POST /api/images/upload` - 30/minute
- `DELETE /api/images/*` - 60/minute
- `POST /api/templates/create-note` - 60/minute
- `POST /api/plugins/*/toggle` - 60/minute
### Path Security
All file operations validate that paths are within the configured `notes_dir` to prevent directory traversal attacks. This includes:
- Note operations
- Folder operations
- Image operations
- Template operations
---
💡 **Tip:** Use the `/api` endpoint to get a live, self-documented list of all available endpoints! 💡 **Tip:** Use the `/api` endpoint to get a live, self-documented list of all available endpoints!

View File

@ -99,6 +99,7 @@ function noteApp() {
// Dropdown state // Dropdown state
showNewDropdown: false, showNewDropdown: false,
dropdownTargetFolder: '', // Folder context for "New" dropdown (empty = root)
// Template state // Template state
showTemplateModal: false, showTemplateModal: false,
@ -620,15 +621,16 @@ function noteApp() {
} }
try { try {
// Determine the note path based on current context // Determine the note path based on dropdown context
let notePath = this.newTemplateNoteName.trim(); let notePath = this.newTemplateNoteName.trim();
if (!notePath.endsWith('.md')) { if (!notePath.endsWith('.md')) {
notePath += '.md'; notePath += '.md';
} }
// If we're in a folder on the homepage, create in that folder // If we have a target folder context, create in that folder
if (this.selectedHomepageFolder) { if (this.dropdownTargetFolder || this.selectedHomepageFolder) {
notePath = `${this.selectedHomepageFolder}/${notePath}`; const targetFolder = this.dropdownTargetFolder || this.selectedHomepageFolder;
notePath = `${targetFolder}/${notePath}`;
} }
// Create note from template // Create note from template
@ -980,17 +982,11 @@ function noteApp() {
</div> </div>
<div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" @click.stop> <div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" @click.stop>
<button <button
@click="createNote('${folder.path.replace(/'/g, "\\'")}')" @click="dropdownTargetFolder = '${folder.path.replace(/'/g, "\\'")}'; showNewDropdown = !showNewDropdown"
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110" class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
style="background-color: var(--bg-tertiary); color: var(--text-secondary);" style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
title="New note here" title="Add item here"
>📄</button> >+</button>
<button
@click="createFolder('${folder.path.replace(/'/g, "\\'")}')"
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
title="New subfolder"
>📁</button>
<button <button
@click="renameFolder('${folder.path.replace(/'/g, "\\'")}', '${folder.name.replace(/'/g, "\\'")}')" @click="renameFolder('${folder.path.replace(/'/g, "\\'")}', '${folder.name.replace(/'/g, "\\'")}')"
class="px-1.5 py-0.5 text-xs rounded hover:brightness-110" class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
@ -1971,21 +1967,27 @@ function noteApp() {
toggleNewDropdown() { toggleNewDropdown() {
this.showNewDropdown = !this.showNewDropdown; this.showNewDropdown = !this.showNewDropdown;
if (this.showNewDropdown) {
this.dropdownTargetFolder = ''; // Set to root when opening main dropdown
}
}, },
closeDropdown() { closeDropdown() {
this.showNewDropdown = false; this.showNewDropdown = false;
this.dropdownTargetFolder = ''; // Reset folder context
}, },
// ===================================================== // =====================================================
// UNIFIED CREATION FUNCTIONS (reusable from anywhere) // UNIFIED CREATION FUNCTIONS (reusable from anywhere)
// ===================================================== // =====================================================
async createNote(folderPath = '') { async createNote(folderPath = null) {
// Use provided folder path, or dropdown target folder context
const targetFolder = folderPath !== null ? folderPath : this.dropdownTargetFolder;
this.closeDropdown(); this.closeDropdown();
const promptText = folderPath const promptText = targetFolder
? `Create note in "${folderPath}".\nEnter note name:` ? `Create note in "${targetFolder}".\nEnter note name:`
: 'Enter note name (you can use folder/name):'; : 'Enter note name (you can use folder/name):';
const noteName = prompt(promptText); const noteName = prompt(promptText);
@ -1998,8 +2000,8 @@ function noteApp() {
} }
let notePath; let notePath;
if (folderPath) { if (targetFolder) {
notePath = `${folderPath}/${sanitizedName}.md`; notePath = `${targetFolder}/${sanitizedName}.md`;
} else { } else {
notePath = sanitizedName.endsWith('.md') ? sanitizedName : `${sanitizedName}.md`; notePath = sanitizedName.endsWith('.md') ? sanitizedName : `${sanitizedName}.md`;
} }
@ -2019,8 +2021,8 @@ function noteApp() {
}); });
if (response.ok) { if (response.ok) {
if (folderPath) { if (targetFolder) {
this.expandedFolders.add(folderPath); this.expandedFolders.add(targetFolder);
} }
await this.loadNotes(); await this.loadNotes();
await this.loadNote(notePath); await this.loadNote(notePath);
@ -2032,11 +2034,13 @@ function noteApp() {
} }
}, },
async createFolder(parentPath = '') { async createFolder(parentPath = null) {
// Use provided parent path, or dropdown target folder context
const targetFolder = parentPath !== null ? parentPath : this.dropdownTargetFolder;
this.closeDropdown(); this.closeDropdown();
const promptText = parentPath const promptText = targetFolder
? `Create subfolder in "${parentPath}".\nEnter folder name:` ? `Create subfolder in "${targetFolder}".\nEnter folder name:`
: 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):'; : 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):';
const folderName = prompt(promptText); const folderName = prompt(promptText);
@ -2048,7 +2052,7 @@ function noteApp() {
return; return;
} }
const folderPath = parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName; const folderPath = targetFolder ? `${targetFolder}/${sanitizedName}` : sanitizedName;
// Check if folder already exists // Check if folder already exists
const existingFolder = this.allFolders.find(folder => folder === folderPath); const existingFolder = this.allFolders.find(folder => folder === folderPath);
@ -2065,8 +2069,8 @@ function noteApp() {
}); });
if (response.ok) { if (response.ok) {
if (parentPath) { if (targetFolder) {
this.expandedFolders.add(parentPath); this.expandedFolders.add(targetFolder);
} }
this.expandedFolders.add(folderPath); this.expandedFolders.add(folderPath);
await this.loadNotes(); await this.loadNotes();