implement instance editing capability and move user creation to a modal

This commit is contained in:
CPTN Cosmo 2026-04-18 17:19:13 +02:00
parent b185c7a70b
commit 2e160cdd14
3 changed files with 123 additions and 35 deletions

30
main.py
View file

@ -7,6 +7,7 @@ import httpx
import base64 import base64
import secrets import secrets
import urllib.parse import urllib.parse
from typing import Optional
from fastapi import FastAPI, Request, Depends, HTTPException from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import Response, FileResponse, JSONResponse from fastapi.responses import Response, FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -328,6 +329,35 @@ def create_instance(inst: InstanceCreate, request: Request):
conn.close() conn.close()
return {"status": "ok"} return {"status": "ok"}
@app.put("/api/instances/{id}", dependencies=[Depends(is_authenticated)])
def update_instance(id: int, inst: InstanceUpdate, request: Request):
user_id = request.session.get("user_id")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# Verify ownership
c.execute('SELECT id FROM instances WHERE id=? AND user_id=?', (id, user_id))
if not c.fetchone():
conn.close()
raise HTTPException(status_code=404, detail="Instance not found")
if inst.secret:
cleaned = clean_secret(inst.secret)
encrypted = fernet.encrypt(cleaned.encode()).decode()
c.execute('''
UPDATE instances SET name=?, ip=?, port=?, encrypted_secret=?
WHERE id=? AND user_id=?
''', (inst.name, inst.ip, inst.port, encrypted, id, user_id))
else:
c.execute('''
UPDATE instances SET name=?, ip=?, port=?
WHERE id=? AND user_id=?
''', (inst.name, inst.ip, inst.port, id, user_id))
conn.commit()
conn.close()
return {"status": "ok"}
@app.get("/api/config", dependencies=[Depends(is_authenticated)]) @app.get("/api/config", dependencies=[Depends(is_authenticated)])
def get_config(): def get_config():
return {"firewall_host_ip": FIREWALL_HOST_IP} return {"firewall_host_ip": FIREWALL_HOST_IP}

View file

@ -19,12 +19,25 @@ document.addEventListener('DOMContentLoaded', () => {
const helpModal = document.getElementById('help-modal'); const helpModal = document.getElementById('help-modal');
const addInstanceModal = document.getElementById('add-instance-modal'); const addInstanceModal = document.getElementById('add-instance-modal');
const showAddBtn = document.getElementById('show-add-btn'); const showAddBtn = document.getElementById('show-add-btn');
const showAddUserBtn = document.getElementById('show-add-user-btn');
const addUserModal = document.getElementById('add-user-modal');
const modalTitle = document.querySelector('#add-instance-modal h2');
const secretInput = document.getElementById('secret');
let config = { firewall_host_ip: null }; let config = { firewall_host_ip: null };
let currentUser = null; let currentUser = null;
let editId = null;
let instancesData = [];
const openModal = (modal) => modal.classList.add('active'); const openModal = (modal) => modal.classList.add('active');
const closeModal = (modal) => modal.classList.remove('active'); const closeModal = (modal) => {
modal.classList.remove('active');
editId = null;
addForm.reset();
modalTitle.textContent = 'Add Instance';
secretInput.placeholder = 'Base32 Secret or otpauth:// URL';
secretInput.required = true;
};
document.querySelectorAll('.btn-close').forEach(btn => { document.querySelectorAll('.btn-close').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@ -37,7 +50,14 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
helpBtn.addEventListener('click', () => openModal(helpModal)); helpBtn.addEventListener('click', () => openModal(helpModal));
showAddBtn.addEventListener('click', () => openModal(addInstanceModal)); showAddBtn.addEventListener('click', () => {
editId = null;
modalTitle.textContent = 'Add Instance';
secretInput.placeholder = 'Base32 Secret or otpauth:// URL';
secretInput.required = true;
openModal(addInstanceModal);
});
showAddUserBtn.addEventListener('click', () => openModal(addUserModal));
const showLogin = () => { const showLogin = () => {
loginOverlay.style.display = 'flex'; loginOverlay.style.display = 'flex';
@ -89,8 +109,8 @@ document.addEventListener('DOMContentLoaded', () => {
try { try {
const res = await fetch('/api/instances'); const res = await fetch('/api/instances');
if (res.status === 401) return showLogin(); if (res.status === 401) return showLogin();
const data = await res.json(); instancesData = await res.json();
renderInstances(data); renderInstances(instancesData);
} catch (e) { } catch (e) {
console.error('Failed to fetch instances:', e); console.error('Failed to fetch instances:', e);
} }
@ -133,6 +153,10 @@ document.addEventListener('DOMContentLoaded', () => {
<span>Last checked: ${timeAgo}</span> <span>Last checked: ${timeAgo}</span>
</div> </div>
</div> </div>
<div class="instance-actions">
<button class="btn btn-icon" onclick="editInstance(${inst.id})" title="Edit">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</button>
<button class="delete-btn" onclick="deleteInstance(${inst.id})" title="Delete"> <button class="delete-btn" onclick="deleteInstance(${inst.id})" title="Delete">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline> <polyline points="3 6 5 6 21 6"></polyline>
@ -140,6 +164,7 @@ document.addEventListener('DOMContentLoaded', () => {
</svg> </svg>
</button> </button>
</div> </div>
</div>
`; `;
}).join(''); }).join('');
}; };
@ -169,27 +194,44 @@ document.addEventListener('DOMContentLoaded', () => {
const port = parseInt(document.getElementById('port').value, 10); const port = parseInt(document.getElementById('port').value, 10);
const secret = document.getElementById('secret').value; const secret = document.getElementById('secret').value;
const url = editId ? `/api/instances/${editId}` : '/api/instances';
const method = editId ? 'PUT' : 'POST';
const body = { name, ip, port };
if (secret) body.secret = secret;
try { try {
const res = await fetch('/api/instances', { const res = await fetch(url, {
method: 'POST', method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, ip, port, secret }) body: JSON.stringify(body)
}); });
if (res.ok) { if (res.ok) {
addForm.reset();
document.getElementById('port').value = "4646";
closeModal(addInstanceModal); closeModal(addInstanceModal);
fetchInstances(); fetchInstances();
} else if (res.status === 401) { } else if (res.status === 401) {
showLogin(); showLogin();
} else { } else {
alert('Failed to add instance'); alert(`Failed to ${editId ? 'update' : 'add'} instance`);
} }
} catch (e) { } catch (e) {
console.error('Error adding instance', e); console.error('Error saving instance', e);
} }
}); });
window.editInstance = (id) => {
const inst = instancesData.find(i => i.id === id);
if (!inst) return;
editId = id;
document.getElementById('name').value = inst.name;
document.getElementById('ip').value = inst.ip;
document.getElementById('port').value = inst.port;
secretInput.value = '';
secretInput.required = false;
secretInput.placeholder = 'Leave blank to keep current secret';
modalTitle.textContent = 'Edit Instance';
openModal(addInstanceModal);
};
loginForm.addEventListener('submit', async (e) => { loginForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const username = document.getElementById('login-username').value; const username = document.getElementById('login-username').value;
@ -248,6 +290,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
if (res.ok) { if (res.ok) {
createUserForm.reset(); createUserForm.reset();
closeModal(addUserModal);
fetchUsers(); fetchUsers();
} else { } else {
const data = await res.json(); const data = await res.json();

View file

@ -68,24 +68,13 @@
<div id="admin-section" class="tab-content"> <div id="admin-section" class="tab-content">
<div class="card user-management-card"> <div class="card user-management-card">
<div class="instances-header">
<h2>User Management</h2> <h2>User Management</h2>
<form id="create-user-form" class="user-create-form"> <button class="btn btn-primary btn-sm" id="show-add-user-btn">
<div class="form-group"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
<label for="new-username">Username</label> Add User
<input type="text" id="new-username" placeholder="Enter username" required> </button>
</div> </div>
<div class="form-group">
<label for="new-password">Password</label>
<input type="password" id="new-password" placeholder="Enter password" required>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" id="new-is-admin">
<span class="checkbox-label">Grant Administrator Privileges</span>
</label>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
<div id="users-list" class="users-list"> <div id="users-list" class="users-list">
<!-- Users injected via JS --> <!-- Users injected via JS -->
</div> </div>
@ -140,6 +129,32 @@
</div> </div>
</div> </div>
<div id="add-user-modal" class="modal">
<div class="modal-content card">
<div class="modal-header">
<h2>Add User</h2>
<button class="btn-close">&times;</button>
</div>
<form id="create-user-form" class="user-create-form">
<div class="form-group">
<label for="new-username">Username</label>
<input type="text" id="new-username" placeholder="Enter username" required>
</div>
<div class="form-group">
<label for="new-password">Password</label>
<input type="password" id="new-password" placeholder="Enter password" required>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" id="new-is-admin">
<span class="checkbox-label">Grant Administrator Privileges</span>
</label>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
</div>
</div>
<div id="help-modal" class="modal"> <div id="help-modal" class="modal">
<div class="modal-content card"> <div class="modal-content card">
<div class="modal-header"> <div class="modal-header">