Add optional delete password and dark theme

This commit is contained in:
2025-10-30 22:55:10 +01:00
parent 0415df96b7
commit 0c670ed843
3 changed files with 285 additions and 34 deletions

34
main.py
View File

@@ -232,7 +232,7 @@ def init_db():
class DeletionRequest(BaseModel):
password: str
password: Optional[str] = None
@app.get("/api/standard-items/{lang}")
@@ -517,7 +517,7 @@ async def mark_item(item_id: int, current_user: User = Depends(get_current_activ
@app.post("/api/items/delete-marked")
async def delete_marked_items(request: DeletionRequest, current_user: User = Depends(get_current_active_user)):
async def delete_marked_items(request: Optional[DeletionRequest] = None, current_user: User = Depends(get_current_active_user)):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute(
@@ -525,9 +525,14 @@ async def delete_marked_items(request: DeletionRequest, current_user: User = Dep
result = cursor.fetchone()
conn.close()
if not result or not pwd_context.verify(request.password, result[0]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password")
# If a deletion password is set in app_settings
if result:
# And no password was provided in the request, or the provided password is incorrect
if not request or not request.password or not pwd_context.verify(request.password, result[0]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password")
# If no deletion password is set in app_settings, then no password is required for deletion.
# The request.password is ignored in this case.
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
@@ -542,7 +547,13 @@ async def get_deletion_password(current_user: User = Depends(get_current_active_
if not current_user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can access this setting")
return {"status": "ok"}
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute(
"SELECT value FROM app_settings WHERE key = 'deletion_password'")
result = cursor.fetchone()
conn.close()
return {"is_set": result is not None}
@app.post("/api/settings/deletion-password")
@@ -551,11 +562,16 @@ async def set_deletion_password(request: DeletionRequest, current_user: User = D
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can set the deletion password")
hashed_password = pwd_context.hash(request.password)
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)",
('deletion_password', hashed_password))
if not request.password:
cursor.execute("DELETE FROM app_settings WHERE key = 'deletion_password'")
else:
hashed_password = pwd_context.hash(request.password)
cursor.execute("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)",
('deletion_password', hashed_password))
conn.commit()
conn.close()
return {"status": "ok"}

View File

@@ -8,8 +8,118 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<link rel="icon" type="image/png" href="/favicon.png">
<style>
:root {
--background-gradient-start: #ece9e6;
--background-gradient-end: #ffffff;
--text-color: #212529;
--card-background-color: #ffffff;
--card-shadow-color: rgba(0, 0, 0, 0.05);
--input-background-color: #ffffff;
--input-text-color: #212529;
--status-bar-background-color: #f8f9fa;
--status-bar-border-color: #dee2e6;
--list-group-item-background-color: #ffffff;
}
[data-theme="dark"] {
--background-gradient-start: #212529;
--background-gradient-end: #343a40;
--text-color: #f8f9fa;
--card-background-color: #343a40;
--card-shadow-color: rgba(255, 255, 255, 0.05);
--input-background-color: #495057;
--input-text-color: #f8f9fa;
--status-bar-background-color: #343a40;
--status-bar-border-color: #495057;
--list-group-item-background-color: #343a40;
}
[data-theme="dark"] .text-dark {
color: var(--text-color) !important;
}
[data-theme="dark"] .modal-content {
background-color: var(--card-background-color);
}
[data-theme="dark"] .form-label,
[data-theme="dark"] .card-title,
[data-theme="dark"] #create-user-status,
[data-theme="dark"] #change-password-status,
[data-theme="dark"] #set-deletion-password-status {
color: var(--text-color);
}
[data-theme="dark"] .btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
[data-theme="dark"] .btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
}
[data-theme="dark"] .btn-outline-primary:hover {
background-color: #0d6efd;
color: #fff;
}
[data-theme="dark"] .btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
[data-theme="dark"] .btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
[data-theme="dark"] .btn-outline-secondary:hover {
background-color: #6c757d;
color: #fff;
}
[data-theme="dark"] #admin-panel-title,
[data-theme="dark"] #logout-btn,
[data-theme="dark"] #item-name,
[data-theme="dark"] #info-text-existing,
[data-theme="dark"] #info-text-new {
color: var(--text-color) !important;
}
[data-theme="dark"] #is-admin-label {
color: var(--text-color) !important;
}
[data-theme="dark"] #item-name::placeholder,
[data-theme="dark"] #deletion-password::placeholder {
color: var(--text-color) !important;
opacity: 0.5;
}
[data-theme="dark"] .form-check-input {
background-color: var(--input-background-color);
border-color: var(--status-bar-border-color);
}
[data-theme="dark"] #password-set-message p {
color: var(--text-color);
}
[data-theme="dark"] hr {
border: 0;
border-top: 1px solid var(--status-bar-border-color);
opacity: 1;
}
[data-theme="dark"] #toggle-deletion-password svg {
fill: #fff;
}
body {
background: linear-gradient(to right, #ece9e6, #ffffff);
background: linear-gradient(to right, var(--background-gradient-start), var(--background-gradient-end));
color: var(--text-color);
}
.container {
max-width: 800px;
@@ -27,6 +137,8 @@
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--list-group-item-background-color);
color: var(--text-color);
}
.delete-btn {
cursor: pointer;
@@ -38,11 +150,11 @@
bottom: 0;
left: 0;
width: 100%;
background-color: #f8f9fa;
background-color: var(--status-bar-background-color);
padding: 5px;
text-align: center;
font-size: 12px;
border-top: 1px solid #dee2e6;
border-top: 1px solid var(--status-bar-border-color);
}
#suggestion-info {
font-size: 0.9rem;
@@ -59,11 +171,32 @@
#login-view, #app-view {
display: none; /* Hidden by default */
}
#login-view, #app-view {
display: none; /* Hidden by default */
.card {
background-color: var(--card-background-color);
box-shadow: 0 0.125rem 0.25rem var(--card-shadow-color);
}
</style> </style>
.form-control {
background-color: var(--input-background-color);
color: var(--input-text-color);
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--input-text-color);
}
</style>
</head>
<body class="pb-5">
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
} else {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}
})();
</script>
<!-- Login View -->
<div id="login-view" class="container mt-5">
@@ -103,6 +236,7 @@
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
<button id="theme-switcher" class="btn btn-sm btn-outline-secondary me-2">☀️</button>
<button id="logout-btn" class="btn btn-sm btn-outline-secondary">Logout</button>
</div>
</div>
@@ -215,11 +349,28 @@
<hr class="my-4">
<h5 id="set-deletion-password-title" class="card-title"></h5>
<div id="password-set-message" style="display: none;">
<p>Passwort ist gesetzt.</p>
<button id="change-password-btn" class="btn btn-primary">Passwort ändern</button>
<button id="delete-password-btn" class="btn btn-danger">Passwort löschen</button>
</div>
<form id="set-deletion-password-form">
<div class="row">
<div class="col-md-6 mb-3">
<label id="deletion-password-label" for="deletion-password" class="form-label"></label>
<input type="password" id="deletion-password" class="form-control" required>
<div class="input-group">
<input type="password" id="deletion-password" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" id="toggle-deletion-password">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.12 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16" style="display: none;">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.94 5.94 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.707z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822z"/>
<path d="M.5 8a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z"/>
</svg>
</button>
</div>
</div>
</div>
<button id="set-deletion-password-button" type="submit" class="btn btn-primary"></button>
@@ -314,6 +465,7 @@
let currentShoppingList = [];
let currentUser = {};
let socket;
let isDeletionPasswordSet = false; // New global variable
// --- WebSocket Functions ---
function connectWebSocket() {
@@ -511,7 +663,7 @@
document.getElementById('change-password-button').textContent = translations.change_password_button;
document.getElementById('manage-users-title').textContent = translations.manage_users_title;
document.getElementById('set-deletion-password-title').textContent = translations.set_deletion_password_title;
document.getElementById('deletion-password-label').textContent = translations.deletion_password_label;
document.getElementById('deletion-password').placeholder = translations.deletion_password_placeholder;
document.getElementById('set-deletion-password-button').textContent = translations.set_deletion_password_button;
}
@@ -754,17 +906,8 @@
}
}
deleteMarkedBtn.addEventListener('click', () => {
deletePasswordModal.show();
});
confirmDeleteBtn.addEventListener('click', async () => {
const password = deletePasswordInput.value;
if (!password) {
deletePasswordError.textContent = translations.password_required;
return;
}
// New function to call the delete marked items API
async function callDeleteMarkedItemsApi(password) {
const response = await fetch('/api/items/delete-marked', {
method: 'POST',
headers: getAuthHeaders(),
@@ -772,13 +915,40 @@
});
if (response.status === 401) {
deletePasswordError.textContent = translations.incorrect_password;
const errorData = await response.json(); // Get the error detail from the response
if (errorData.detail === "Incorrect password") {
deletePasswordError.textContent = translations.incorrect_password;
} else {
handleLogout(); // This is a session issue
}
} else if (response.ok) {
deletePasswordModal.hide();
await fetchItems();
} else {
deletePasswordError.textContent = translations.generic_error;
}
}
deleteMarkedBtn.addEventListener('click', () => {
deletePasswordModal.show();
});
deletePasswordForm.addEventListener('submit', (event) => {
event.preventDefault();
confirmDeleteBtn.click();
});
confirmDeleteBtn.addEventListener('click', async () => {
const password = deletePasswordInput.value;
// Always send the password (can be empty) to the backend.
// The backend will determine if a password is required.
await callDeleteMarkedItemsApi(password);
});
// Clear password input on modal close
deletePasswordModal._element.addEventListener('hidden.bs.modal', () => {
deletePasswordInput.value = '';
deletePasswordError.textContent = '';
});
async function getDeletionPassword() {
@@ -786,8 +956,35 @@
const response = await fetch('/api/settings/deletion-password', { headers: getAuthHeaders() });
if (response.status === 401) return handleLogout();
const data = await response.json();
isDeletionPasswordSet = data.is_set; // Update the global variable
if (data.is_set) {
document.getElementById('password-set-message').style.display = 'block';
document.getElementById('set-deletion-password-form').style.display = 'none';
} else {
document.getElementById('password-set-message').style.display = 'none';
document.getElementById('set-deletion-password-form').style.display = 'block';
}
}
document.getElementById('change-password-btn').addEventListener('click', () => {
document.getElementById('password-set-message').style.display = 'none';
document.getElementById('set-deletion-password-form').style.display = 'block';
});
document.getElementById('delete-password-btn').addEventListener('click', async () => {
const response = await fetch('/api/settings/deletion-password', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ password: '' })
});
if (response.ok) {
await getDeletionPassword();
}
});
async function setDeletionPassword(event) {
event.preventDefault();
const password = deletionPasswordInput.value;
@@ -803,6 +1000,7 @@
if (response.ok) {
setDeletionPasswordStatus.textContent = translations.password_set_success;
setDeletionPasswordStatus.className = 'mt-2 text-success';
await getDeletionPassword();
} else {
const data = await response.json();
setDeletionPasswordStatus.textContent = `Error: ${data.detail}`;
@@ -847,6 +1045,24 @@
setDeletionPasswordForm.addEventListener('submit', setDeletionPassword);
const toggleDeletionPassword = document.getElementById('toggle-deletion-password');
toggleDeletionPassword.addEventListener('click', () => {
const passwordInput = document.getElementById('deletion-password');
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
const eyeIcon = toggleDeletionPassword.querySelector('.bi-eye');
const eyeSlashIcon = toggleDeletionPassword.querySelector('.bi-eye-slash');
if (type === 'password') {
eyeIcon.style.display = 'block';
eyeSlashIcon.style.display = 'none';
} else {
eyeIcon.style.display = 'none';
eyeSlashIcon.style.display = 'block';
}
});
// --- Initialisierung ---
async function initApp() {
// NOTE: Translations are now fetched before this function is called.
@@ -989,6 +1205,23 @@
main(); // Run the app
const themeSwitcher = document.getElementById('theme-switcher');
themeSwitcher.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeSwitcher.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
// Set initial theme switcher icon
const currentTheme = document.documentElement.getAttribute('data-theme');
if (currentTheme === 'dark') {
themeSwitcher.textContent = '☀️';
} else {
themeSwitcher.textContent = '🌙';
}
</script>

View File

@@ -69,7 +69,7 @@ def get_all_translations(lang: str) -> Dict[str, str]:
"login_error_incorrect": "Benutzername oder Passwort inkorrekt.",
"login_error_generic": "Ein Fehler ist aufgetreten. Bitte erneut versuchen.",
"set_deletion_password_title": "Löschpasswort festlegen",
"deletion_password_label": "Löschpasswort",
"deletion_password_placeholder": "Löschpasswort (Optional)",
"set_deletion_password_button": "Passwort festlegen",
"password_set_success": "Passwort erfolgreich festgelegt.",
"delete_password_modal_title": "Löschen bestätigen",
@@ -77,7 +77,8 @@ def get_all_translations(lang: str) -> Dict[str, str]:
"cancel_button": "Abbrechen",
"password_required": "Passwort erforderlich.",
"incorrect_password": "Falsches Passwort.",
"generic_error": "Ein Fehler ist aufgetreten."
"generic_error": "Ein Fehler ist aufgetreten.",
"admin_permission_required": "Administratorberechtigung zum Löschen von Elementen erforderlich."
}
else: # Fallback to English
return {
@@ -118,7 +119,7 @@ def get_all_translations(lang: str) -> Dict[str, str]:
"login_error_incorrect": "Incorrect username or password.",
"login_error_generic": "An error occurred. Please try again.",
"set_deletion_password_title": "Set Deletion Password",
"deletion_password_label": "Deletion Password",
"deletion_password_placeholder": "Deletion Password (Optional)",
"set_deletion_password_button": "Set Password",
"password_set_success": "Password set successfully.",
"delete_password_modal_title": "Confirm Deletion",
@@ -126,5 +127,6 @@ def get_all_translations(lang: str) -> Dict[str, str]:
"cancel_button": "Cancel",
"password_required": "Password required.",
"incorrect_password": "Incorrect password.",
"generic_error": "An error occurred."
"generic_error": "An error occurred.",
"admin_permission_required": "Admin permission required to delete items."
}