Add optional delete password and dark theme
This commit is contained in:
34
main.py
34
main.py
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user