user authentication
This commit is contained in:
294
web/templates/admin.html
Normal file
294
web/templates/admin.html
Normal file
@@ -0,0 +1,294 @@
|
||||
{{define "admin"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_content"}}
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||
<p class="text-gray-400">Manage users, groups, and permissions</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Users -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-users mr-2"></i>Users
|
||||
</h2>
|
||||
<!-- Create User -->
|
||||
<form id="form-create-user" class="mb-4 grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||
<input name="username" type="text" class="form-input" placeholder="Username" required />
|
||||
<input name="email" type="email" class="form-input" placeholder="Email" required />
|
||||
<input name="password" type="password" class="form-input" placeholder="Password" required />
|
||||
<button type="submit" class="btn-primary text-sm"><i class="fas fa-plus mr-2"></i>Create</button>
|
||||
</form>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-300">
|
||||
<th class="px-3 py-2">Username</th>
|
||||
<th class="px-3 py-2">Email</th>
|
||||
<th class="px-3 py-2">Status</th>
|
||||
<th class="px-3 py-2">MFA</th>
|
||||
<th class="px-3 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .users}}
|
||||
{{range .users}}
|
||||
<tr class="border-t border-gray-700" data-user-id="{{.ID}}" data-username="{{.Username}}" data-has-mfa="{{if .MFASecret.Valid}}1{{else}}0{{end}}">
|
||||
<td class="px-3 py-2 text-white">{{.Username}}</td>
|
||||
<td class="px-3 py-2 text-gray-300">{{.Email}}</td>
|
||||
<td class="px-3 py-2">
|
||||
{{if .IsActive}}<span class="text-green-400">Active</span>{{else}}<span class="text-red-400">Disabled</span>{{end}}
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
{{if .MFASecret.Valid}}<span class="text-green-400">Enabled</span>{{else}}<span class="text-gray-400">None</span>{{end}}
|
||||
</td>
|
||||
<td class="px-3 py-2 space-x-1">
|
||||
{{if ne .ID $.CurrentUserID}}
|
||||
{{if .IsActive}}
|
||||
<button class="btn-secondary text-xs px-2 py-1" data-action="user-deactivate" title="Disable account">Disable</button>
|
||||
{{else}}
|
||||
<button class="btn-primary text-xs px-2 py-1" data-action="user-activate" title="Enable account">Enable</button>
|
||||
{{end}}
|
||||
{{if .MFASecret.Valid}}
|
||||
<button class="btn-secondary text-xs px-2 py-1" data-action="mfa-disable" title="Disable MFA">MFA Off</button>
|
||||
<button class="btn-warning text-xs px-2 py-1" data-action="mfa-reset" title="Reset MFA secret">MFA Reset</button>
|
||||
{{else}}
|
||||
<button class="btn-primary text-xs px-2 py-1" data-action="mfa-enable" title="Enable MFA">MFA On</button>
|
||||
{{end}}
|
||||
<button class="btn-danger text-xs px-2 py-1" data-action="delete-user" title="Delete user"><i class="fas fa-trash"></i></button>
|
||||
{{else}}
|
||||
<span class="text-xs text-gray-500">Actions disabled for current user</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="5" class="px-3 py-4 text-gray-400">No users found.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-user-group mr-2"></i>Groups
|
||||
</h2>
|
||||
<form id="form-create-group" class="mb-4 grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||
<input name="name" type="text" class="form-input md:col-span-3" placeholder="Group name" required />
|
||||
<button type="submit" class="btn-primary text-sm"><i class="fas fa-plus mr-2"></i>Create</button>
|
||||
</form>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-300">
|
||||
<th class="px-3 py-2">Name</th>
|
||||
<th class="px-3 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .groups}}
|
||||
{{range .groups}}
|
||||
<tr class="border-t border-gray-700" data-group-id="{{.ID}}" data-group-name="{{.Name}}">
|
||||
<td class="px-3 py-2 text-white">{{.Name}}</td>
|
||||
<td class="px-3 py-2">
|
||||
<button class="btn-danger text-xs px-2 py-1" data-action="delete-group" title="Delete group"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="2" class="px-3 py-4 text-gray-400">No groups found.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions and Memberships -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-key mr-2"></i>Permissions</h2>
|
||||
<div class="overflow-x-auto mb-6">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-300">
|
||||
<th class="px-3 py-2">Group</th>
|
||||
<th class="px-3 py-2">Path</th>
|
||||
<th class="px-3 py-2">Read</th>
|
||||
<th class="px-3 py-2">Write</th>
|
||||
<th class="px-3 py-2">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .permissions}}
|
||||
{{range .permissions}}
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="px-3 py-2 text-white">{{.Group}}</td>
|
||||
<td class="px-3 py-2"><code class="bg-gray-900 px-1 py-0.5 rounded">{{.Path}}</code></td>
|
||||
<td class="px-3 py-2">{{if .CanRead}}✅{{else}}❌{{end}}</td>
|
||||
<td class="px-3 py-2">{{if .CanWrite}}✅{{else}}❌{{end}}</td>
|
||||
<td class="px-3 py-2">{{if .CanDelete}}✅{{else}}❌{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="5" class="px-3 py-4 text-gray-400">No permissions configured.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 class="text-white font-semibold mb-3">Manage Memberships</h3>
|
||||
<form id="form-add-membership" class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-3">
|
||||
<select name="user_id" class="form-input" required>
|
||||
<option value="" disabled selected>Select user</option>
|
||||
{{range .users}}<option value="{{.ID}}">{{.Username}}</option>{{end}}
|
||||
</select>
|
||||
<select name="group_id" class="form-input" required>
|
||||
<option value="" disabled selected>Select group</option>
|
||||
{{range .groups}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn-primary text-sm"><i class="fas fa-user-plus mr-2"></i>Add to Group</button>
|
||||
</form>
|
||||
<form id="form-remove-membership" class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<select name="user_id" class="form-input" required>
|
||||
<option value="" disabled selected>Select user</option>
|
||||
{{range .users}}<option value="{{.ID}}">{{.Username}}</option>{{end}}
|
||||
</select>
|
||||
<select name="group_id" class="form-input" required>
|
||||
<option value="" disabled selected>Select group</option>
|
||||
{{range .groups}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn-danger text-sm"><i class="fas fa-user-minus mr-2"></i>Remove from Group</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_scripts"}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const getCSRF = () => {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
};
|
||||
|
||||
// Create user
|
||||
const formCreateUser = document.getElementById('form-create-user');
|
||||
if (formCreateUser) {
|
||||
formCreateUser.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formCreateUser);
|
||||
const res = await fetch('/editor/admin/users', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('User created', 'success'); window.location.reload(); }
|
||||
else { showNotification('Create user failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
}
|
||||
|
||||
// Delete user
|
||||
document.querySelectorAll('[data-action="delete-user"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const row = e.currentTarget.closest('[data-user-id]');
|
||||
const id = row && row.getAttribute('data-user-id');
|
||||
const username = (row && row.getAttribute('data-username')) || '';
|
||||
if (!id) return;
|
||||
if (username === 'admin') { showNotification('Cannot delete default admin user', 'error'); return; }
|
||||
if (!confirm('Delete user ' + username + ' ?')) return;
|
||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('User deleted', 'success'); window.location.reload(); }
|
||||
else { showNotification('Delete user failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
});
|
||||
|
||||
// Activate/deactivate user
|
||||
document.querySelectorAll('[data-action="user-activate"], [data-action="user-deactivate"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const row = e.currentTarget.closest('[data-user-id]');
|
||||
const id = row && row.getAttribute('data-user-id');
|
||||
const action = e.currentTarget.getAttribute('data-action');
|
||||
const active = action === 'user-activate' ? '1' : '0';
|
||||
const fd = new FormData();
|
||||
fd.set('active', active);
|
||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id) + '/active', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('User status updated', 'success'); window.location.reload(); }
|
||||
else { showNotification('Update status failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
});
|
||||
|
||||
// MFA actions
|
||||
const mfaRequest = async (row, path, okMsg) => {
|
||||
const id = row && row.getAttribute('data-user-id');
|
||||
const res = await fetch('/editor/admin/users/' + encodeURIComponent(id) + path, { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification(okMsg, 'success'); window.location.reload(); }
|
||||
else { showNotification('MFA action failed: ' + (data.error || res.statusText), 'error'); }
|
||||
};
|
||||
document.querySelectorAll('[data-action="mfa-enable"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/enable', 'MFA enabled')));
|
||||
document.querySelectorAll('[data-action="mfa-disable"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/disable', 'MFA disabled')));
|
||||
document.querySelectorAll('[data-action="mfa-reset"]').forEach(btn => btn.addEventListener('click', (e) => mfaRequest(e.currentTarget.closest('[data-user-id]'), '/mfa/reset', 'MFA reset')));
|
||||
|
||||
// Create group
|
||||
const formCreateGroup = document.getElementById('form-create-group');
|
||||
if (formCreateGroup) {
|
||||
formCreateGroup.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formCreateGroup);
|
||||
const res = await fetch('/editor/admin/groups', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('Group created', 'success'); window.location.reload(); }
|
||||
else { showNotification('Create group failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
}
|
||||
|
||||
// Delete group
|
||||
document.querySelectorAll('[data-action="delete-group"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const card = e.currentTarget.closest('[data-group-id]');
|
||||
const id = card && card.getAttribute('data-group-id');
|
||||
const name = (card && card.getAttribute('data-group-name')) || '';
|
||||
if (!id) return;
|
||||
if (name === 'admin' || name === 'public') { showNotification('Cannot delete core group: ' + name, 'error'); return; }
|
||||
if (!confirm('Delete group ' + name + ' ?')) return;
|
||||
const res = await fetch('/editor/admin/groups/' + encodeURIComponent(id), { method: 'DELETE', headers: { 'X-CSRF-Token': getCSRF() } });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('Group deleted', 'success'); window.location.reload(); }
|
||||
else { showNotification('Delete group failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
});
|
||||
|
||||
// Add membership
|
||||
const formAddMem = document.getElementById('form-add-membership');
|
||||
if (formAddMem) {
|
||||
formAddMem.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formAddMem);
|
||||
const res = await fetch('/editor/admin/memberships/add', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('User added to group', 'success'); }
|
||||
else { showNotification('Add membership failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
}
|
||||
|
||||
// Remove membership
|
||||
const formRemMem = document.getElementById('form-remove-membership');
|
||||
if (formRemMem) {
|
||||
formRemMem.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(formRemMem);
|
||||
const res = await fetch('/editor/admin/memberships/remove', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: fd });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) { showNotification('User removed from group', 'success'); }
|
||||
else { showNotification('Remove membership failed: ' + (data.error || res.statusText), 'error'); }
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -271,9 +271,28 @@
|
||||
<button id="open-search" class="text-gray-400 hover:text-white transition-colors" title="Search" aria-label="Search">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</button>
|
||||
<a href="/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
{{if .Authenticated}}
|
||||
{{if .IsAdmin}}
|
||||
<a href="/editor/admin" class="text-gray-400 hover:text-white transition-colors" title="Admin">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
<a href="/editor/profile" class="text-gray-400 hover:text-white transition-colors" title="Profile">
|
||||
<i class="fas fa-user"></i>
|
||||
</a>
|
||||
<a href="/editor/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
|
||||
<i class="fas fa-gear"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Authenticated}}
|
||||
<button id="logout-btn" class="text-gray-400 hover:text-white transition-colors" title="Logout">
|
||||
<i class="fas fa-right-from-bracket"></i>
|
||||
</button>
|
||||
{{else}}
|
||||
<a href="/editor/login" class="text-gray-400 hover:text-white transition-colors" title="Login">
|
||||
<i class="fas fa-right-to-bracket"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<button id="sidebar-toggle" class="toggle-btn" title="Toggle sidebar" aria-label="Toggle sidebar">
|
||||
<i id="sidebar-toggle-icon" class="fas fa-chevron-left"></i>
|
||||
@@ -284,9 +303,11 @@
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="sidebar-content px-4 py-4">
|
||||
<a href="/create" class="btn-primary text-sm w-full text-center">
|
||||
<i class="fas fa-plus mr-2"></i>New Note
|
||||
</a>
|
||||
{{if .Authenticated}}
|
||||
<a href="/editor/create" class="btn-primary text-sm w-full text-center">
|
||||
<i class="fas fa-plus mr-2"></i>New Note
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- File Tree -->
|
||||
@@ -334,8 +355,18 @@
|
||||
{{template "edit_content" .}}
|
||||
{{else if eq .Page "settings"}}
|
||||
{{template "settings_content" .}}
|
||||
{{else if eq .Page "admin"}}
|
||||
{{template "admin_content" .}}
|
||||
{{else if eq .Page "profile"}}
|
||||
{{template "profile_content" .}}
|
||||
{{else if eq .Page "error"}}
|
||||
{{template "error_content" .}}
|
||||
{{else if eq .Page "login"}}
|
||||
{{template "login_content" .}}
|
||||
{{else if eq .Page "mfa"}}
|
||||
{{template "mfa_content" .}}
|
||||
{{else if eq .Page "mfa_setup"}}
|
||||
{{template "mfa_setup_content" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -653,6 +684,29 @@
|
||||
if (e.key === 'Escape' && !searchModal.classList.contains('hidden')) closeSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Logout handler (CSRF protected)
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
const res = await fetch('/editor/logout', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.href = '/editor/login';
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showNotification('Logout failed: ' + (data.error || res.statusText), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Logout error: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -670,8 +724,18 @@
|
||||
{{template "edit_scripts" .}}
|
||||
{{else if eq .Page "settings"}}
|
||||
{{template "settings_scripts" .}}
|
||||
{{else if eq .Page "admin"}}
|
||||
{{template "admin_scripts" .}}
|
||||
{{else if eq .Page "profile"}}
|
||||
{{template "profile_scripts" .}}
|
||||
{{else if eq .Page "error"}}
|
||||
{{template "error_scripts" .}}
|
||||
{{else if eq .Page "login"}}
|
||||
{{template "login_scripts" .}}
|
||||
{{else if eq .Page "mfa"}}
|
||||
{{template "mfa_scripts" .}}
|
||||
{{else if eq .Page "mfa_setup"}}
|
||||
{{template "mfa_setup_scripts" .}}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -93,7 +93,7 @@ console.log('Hello, World!');
|
||||
const editorGrid = document.getElementById('editor-grid');
|
||||
const togglePreviewBtn = document.getElementById('toggle-preview');
|
||||
|
||||
const imageStorageMode = {{.image_storage_mode}};
|
||||
const imageStorageMode = parseInt('{{.image_storage_mode}}', 10) || 1;
|
||||
const imageSubfolderName = "{{.image_subfolder_name}}";
|
||||
const currentFolderPath = "{{.folder_path}}";
|
||||
|
||||
@@ -174,8 +174,12 @@ console.log('Hello, World!');
|
||||
formData.append('content', content);
|
||||
formData.append('folder_path', folderPath);
|
||||
|
||||
fetch('/create', {
|
||||
// CSRF token from cookie
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/create', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
const editorGrid = document.getElementById('editor-grid');
|
||||
const togglePreviewBtn = document.getElementById('toggle-preview');
|
||||
|
||||
const imageStorageMode = {{.image_storage_mode}};
|
||||
const imageStorageMode = parseInt('{{.image_storage_mode}}', 10) || 1;
|
||||
const imageSubfolderName = "{{.image_subfolder_name}}";
|
||||
const currentFolderPath = "{{.folder_path}}";
|
||||
const currentNotePath = "{{.note_path}}";
|
||||
@@ -158,8 +158,12 @@
|
||||
const formData = new FormData();
|
||||
formData.append('content', content);
|
||||
|
||||
fetch('/edit/' + notePath, {
|
||||
// CSRF token from cookie
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/edit/' + notePath, {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -169,7 +169,9 @@
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('content', cm ? cm.getValue() : contentEl.value);
|
||||
fetch('/edit_text/' + filePath, { method: 'POST', body: formData })
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
fetch('/editor/edit_text/' + filePath, { method: 'POST', headers: csrf ? { 'X-CSRF-Token': csrf } : {}, body: formData })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<button id="upload-btn" class="btn-primary">
|
||||
<i class="fas fa-upload mr-2"></i>Upload File
|
||||
</button>
|
||||
<a href="/create?folder={{.folder_path}}" class="btn-secondary">
|
||||
<a href="/editor/create?folder={{.folder_path}}" class="btn-secondary">
|
||||
<i class="fas fa-plus mr-2"></i>New Note
|
||||
</a>
|
||||
</div>
|
||||
@@ -70,12 +70,12 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
{{if eq .Type "md"}}
|
||||
<a href="/edit/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||
<a href="/editor/edit/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if eq .Type "text"}}
|
||||
<a href="/edit_text/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||
<a href="/editor/edit_text/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
@@ -179,8 +179,11 @@
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
fetch('/upload', {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
fetch('/editor/upload', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -237,8 +240,11 @@
|
||||
|
||||
document.getElementById('confirm-delete').addEventListener('click', function() {
|
||||
if (deleteTarget) {
|
||||
fetch('/delete/' + deleteTarget, {
|
||||
method: 'DELETE'
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
fetch('/editor/delete/' + deleteTarget, {
|
||||
method: 'DELETE',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
||||
33
web/templates/login.html
Normal file
33
web/templates/login.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{{define "login"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "login_content"}}
|
||||
<div class="max-w-md mx-auto p-6">
|
||||
<h1 class="text-2xl font-bold text-white mb-4">Sign in</h1>
|
||||
{{if .error}}
|
||||
<div class="bg-red-900/50 border border-red-700 text-red-200 rounded p-3 mb-4">{{.error}}</div>
|
||||
{{end}}
|
||||
<form method="POST" action="/editor/login" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1" for="username">Username or Email</label>
|
||||
<input id="username" name="username" type="text" required class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1" for="password">Password</label>
|
||||
<input id="password" name="password" type="password" required class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit" class="btn-primary"><i class="fas fa-sign-in-alt mr-2"></i>Login</button>
|
||||
<a href="#" class="text-sm text-blue-400 hover:underline">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "login_scripts"}}
|
||||
<script>
|
||||
// optional: add client-side logic here
|
||||
</script>
|
||||
{{end}}
|
||||
35
web/templates/mfa.html
Normal file
35
web/templates/mfa.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{define "mfa"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "mfa_content"}}
|
||||
<div class="max-w-md mx-auto p-6">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Multi‑Factor Authentication</h1>
|
||||
<p class="text-gray-400 mb-6">Enter the 6‑digit code from your authenticator app.</p>
|
||||
|
||||
{{if .error}}
|
||||
<div class="mb-4 p-3 rounded bg-red-700 text-white">{{.error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form id="mfa-form" class="space-y-4" method="POST" action="/editor/mfa">
|
||||
<input type="hidden" name="csrf_token" value="{{.csrf_token}}" />
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-300 mb-2">Authentication code</label>
|
||||
<input id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" class="form-input" placeholder="123456" required />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-primary"><i class="fas fa-right-to-bracket mr-2"></i>Verify</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "mfa_scripts"}}
|
||||
<script>
|
||||
// Optional: autofocus the code field
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const code = document.getElementById('code');
|
||||
if (code) code.focus();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
62
web/templates/mfa_setup.html
Normal file
62
web/templates/mfa_setup.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{{define "mfa_setup"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "mfa_setup_content"}}
|
||||
<div class="max-w-lg mx-auto p-6">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">Set up Multi‑Factor Authentication</h1>
|
||||
<p class="text-gray-400 mb-6">Scan the QR code below with your authenticator app (Google Authenticator, Authy, 1Password, etc.), or enter the secret manually, then enter a code to confirm.</p>
|
||||
|
||||
<div class="bg-slate-800 border border-slate-700 rounded p-4 mb-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<img alt="QR Code" class="bg-white rounded p-2" src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&data={{urlquery .OTPAuthURI}}" width="180" height="180"/>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm text-gray-400">Secret</div>
|
||||
<div class="font-mono text-lg text-white break-all">{{.Secret}}</div>
|
||||
<div class="text-sm text-gray-400 mt-2">URI</div>
|
||||
<div class="text-xs text-gray-300 break-all">{{.OTPAuthURI}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="mfa-verify-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-300 mb-2">Enter 6‑digit code</label>
|
||||
<input id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" class="form-input" placeholder="123456" required />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-primary"><i class="fas fa-check mr-2"></i>Verify and Enable</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "mfa_setup_scripts"}}
|
||||
<script>
|
||||
function getCSRF() {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('mfa-verify-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(form);
|
||||
const params = new URLSearchParams(Array.from(fd.entries()));
|
||||
try {
|
||||
const res = await fetch('/editor/profile/mfa/verify', { method: 'POST', headers: { 'X-CSRF-Token': getCSRF() }, body: params });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) {
|
||||
showNotification('MFA enabled', 'success');
|
||||
window.location.href = '/editor/profile';
|
||||
} else {
|
||||
throw new Error(data.error || res.statusText);
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Verification failed: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -9,15 +9,19 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-3xl font-bold text-white">{{.title}}</h1>
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="/edit/{{.note_path}}" class="btn-primary">
|
||||
{{if .Authenticated}}
|
||||
<a href="/editor/edit/{{.note_path}}" class="btn-primary">
|
||||
<i class="fas fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
{{end}}
|
||||
<a href="/download/{{.note_path}}" class="btn-secondary">
|
||||
<i class="fas fa-download mr-2"></i>Download
|
||||
</a>
|
||||
{{if .Authenticated}}
|
||||
<button class="btn-danger delete-note-btn" data-path="{{.note_path}}">
|
||||
<i class="fas fa-trash mr-2"></i>Delete
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,8 +77,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
const path = deleteBtn.dataset.path;
|
||||
fetch(`/delete/${path}`, {
|
||||
method: 'DELETE'
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
const csrf = m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
fetch(`/editor/delete/${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
|
||||
175
web/templates/profile.html
Normal file
175
web/templates/profile.html
Normal file
@@ -0,0 +1,175 @@
|
||||
{{define "profile"}}
|
||||
{{template "base" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "profile_content"}}
|
||||
<div class="max-w-3xl mx-auto p-6">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Your Profile</h1>
|
||||
<p class="text-gray-400">Manage your account details and security</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Email -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-envelope mr-2"></i>Email</h2>
|
||||
<form id="email-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||
<input type="email" id="email" name="email" value="{{.Email}}"
|
||||
class="form-input" placeholder="you@example.com" required>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save mr-2"></i>Update Email
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-key mr-2"></i>Change Password</h2>
|
||||
<form id="password-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-gray-300 mb-2">Current Password</label>
|
||||
<input type="password" id="current_password" name="current_password" class="form-input" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-300 mb-2">New Password</label>
|
||||
<input type="password" id="new_password" name="new_password" class="form-input" minlength="8" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-300 mb-2">Confirm New Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" class="form-input" minlength="8" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save mr-2"></i>Change Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- MFA -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">
|
||||
<i class="fas fa-shield-halved mr-2"></i>Multi‑Factor Authentication</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-white font-medium">Status: <span id="mfa-status" class="ml-1">{{if .MFAEnabled}}Enabled{{else}}Disabled{{end}}</span></div>
|
||||
<div class="text-sm text-gray-400">Add an extra layer of security to your account</div>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button id="mfa-enable" class="btn-primary {{if .MFAEnabled}}hidden{{end}}"><i class="fas fa-toggle-on mr-2"></i>Enable</button>
|
||||
<button id="mfa-disable" class="btn-danger {{if not .MFAEnabled}}hidden{{end}}"><i class="fas fa-toggle-off mr-2"></i>Disable</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-3">Note: This simple flow generates or clears your MFA secret. A full QR/TOTP enrollment flow can be added later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "profile_scripts"}}
|
||||
<script>
|
||||
function getCSRF() {
|
||||
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return m && m[1] ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
function formToJSON(form) {
|
||||
const fd = new FormData(form);
|
||||
return new URLSearchParams(Array.from(fd.entries()));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const emailForm = document.getElementById('email-form');
|
||||
const passwordForm = document.getElementById('password-form');
|
||||
const btnEnable = document.getElementById('mfa-enable');
|
||||
const btnDisable = document.getElementById('mfa-disable');
|
||||
const mfaStatus = document.getElementById('mfa-status');
|
||||
|
||||
if (emailForm) emailForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const res = await fetch('/editor/profile/email', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
||||
body: formToJSON(emailForm)
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) {
|
||||
showNotification('Email updated', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || res.statusText);
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('Update failed: ' + err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
if (passwordForm) passwordForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const newpw = document.getElementById('new_password').value;
|
||||
const conf = document.getElementById('confirm_password').value;
|
||||
if (newpw !== conf) {
|
||||
showNotification('New passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/editor/profile/password', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'X-CSRF-Token': getCSRF()}),
|
||||
body: formToJSON(passwordForm)
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) {
|
||||
passwordForm.reset();
|
||||
showNotification('Password changed', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || res.statusText);
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('Password change failed: ' + err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function toggleMFA(enable) {
|
||||
try {
|
||||
const url = enable ? '/editor/profile/mfa/enable' : '/editor/profile/mfa/disable';
|
||||
const res = await fetch(url, { method: 'POST', headers: {'X-CSRF-Token': getCSRF()} });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.success) {
|
||||
if (enable) {
|
||||
if (data.setup && data.redirect) {
|
||||
// Enrollment flow: go to setup page; do not toggle UI yet
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
// Direct enable (no setup)
|
||||
btnEnable.classList.add('hidden');
|
||||
btnDisable.classList.remove('hidden');
|
||||
mfaStatus.textContent = 'Enabled';
|
||||
} else {
|
||||
btnDisable.classList.add('hidden');
|
||||
btnEnable.classList.remove('hidden');
|
||||
mfaStatus.textContent = 'Disabled';
|
||||
}
|
||||
showNotification('MFA ' + (enable ? 'enabled' : 'disabled'), 'success');
|
||||
} else {
|
||||
throw new Error(data.error || res.statusText);
|
||||
}
|
||||
} catch (err) {
|
||||
showNotification('MFA update failed: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (btnEnable) btnEnable.addEventListener('click', (e) => { e.preventDefault(); toggleMFA(true); });
|
||||
if (btnDisable) btnDisable.addEventListener('click', (e) => { e.preventDefault(); toggleMFA(false); });
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -179,7 +179,7 @@
|
||||
// Load current settings
|
||||
function loadSettings() {
|
||||
// Load image storage settings
|
||||
fetch('/settings/image_storage')
|
||||
fetch('/editor/settings/image_storage')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.querySelector(`input[name="storage_mode"][value="${data.mode}"]`).checked = true;
|
||||
@@ -190,7 +190,7 @@
|
||||
.catch(error => console.error('Error loading image storage settings:', error));
|
||||
|
||||
// Load notes directory settings
|
||||
fetch('/settings/notes_dir')
|
||||
fetch('/editor/settings/notes_dir')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('notes_dir').value = data.notes_dir || '';
|
||||
@@ -198,7 +198,7 @@
|
||||
.catch(error => console.error('Error loading notes directory settings:', error));
|
||||
|
||||
// Load file extensions settings
|
||||
fetch('/settings/file_extensions')
|
||||
fetch('/editor/settings/file_extensions')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('allowed_image_extensions').value = data.allowed_image_extensions || '';
|
||||
@@ -237,9 +237,12 @@
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/settings/image_storage', {
|
||||
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/settings/image_storage', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -263,9 +266,12 @@
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/settings/notes_dir', {
|
||||
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/settings/notes_dir', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -289,9 +295,12 @@
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/settings/file_extensions', {
|
||||
|
||||
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
||||
|
||||
fetch('/editor/settings/file_extensions', {
|
||||
method: 'POST',
|
||||
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page Works!</h1>
|
||||
<p>App Name: {{.app_name}}</p>
|
||||
<p>Folder Contents Count: {{len .folder_contents}}</p>
|
||||
{{if .folder_contents}}
|
||||
<ul>
|
||||
{{range .folder_contents}}
|
||||
<li>{{.DisplayName}} ({{.Type}})</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-3xl font-bold text-white">{{.file_name}}</h1>
|
||||
<div class="flex items-center space-x-3">
|
||||
{{if .is_editable}}
|
||||
{{if and .Authenticated .is_editable}}
|
||||
<a href="/edit_text/{{.file_path}}" class="btn-primary">
|
||||
<i class="fas fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
@@ -17,9 +17,11 @@
|
||||
<a href="/download/{{.file_path}}" class="btn-secondary">
|
||||
<i class="fas fa-download mr-2"></i>Download
|
||||
</a>
|
||||
{{if .Authenticated}}
|
||||
<button class="btn-danger delete-file-btn" data-path="{{.file_path}}">
|
||||
<i class="fas fa-trash mr-2"></i>Delete
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user