Prevent deletion of family members from contact list
This commit is contained in:
@@ -240,9 +240,11 @@ function renderContactItem(c) {
|
|||||||
class="contact-action-btn" aria-label="${t('contacts.exportLabel')}" title="${t('contacts.exportTooltip')}">
|
class="contact-action-btn" aria-label="${t('contacts.exportLabel')}" title="${t('contacts.exportTooltip')}">
|
||||||
<i data-lucide="download" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<i data-lucide="download" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<button class="contact-action-btn" data-action="delete" data-id="${c.id}" aria-label="${t('contacts.deleteLabel')}">
|
${!c.family_user_id ? `
|
||||||
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<button class="contact-action-btn" data-action="delete" data-id="${c.id}" aria-label="${t('contacts.deleteLabel')}">
|
||||||
</button>
|
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -288,7 +290,7 @@ function openContactModal({ mode, contact = null }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
||||||
${isEdit ? `<button class="btn btn--danger btn--icon" id="cm-delete" aria-label="${t('contacts.deleteLabel')}">
|
${isEdit && !contact.family_user_id ? `<button class="btn btn--danger btn--icon" id="cm-delete" aria-label="${t('contacts.deleteLabel')}">
|
||||||
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
</button>` : '<div></div>'}
|
</button>` : '<div></div>'}
|
||||||
<div style="display:flex;gap:var(--space-3);">
|
<div style="display:flex;gap:var(--space-3);">
|
||||||
|
|||||||
@@ -138,6 +138,13 @@ router.put('/:id', (req, res) => {
|
|||||||
router.delete('/:id', (req, res) => {
|
router.delete('/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
|
const contact = db.get().prepare('SELECT family_user_id FROM contacts WHERE id = ?').get(id);
|
||||||
|
if (!contact) return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
|
||||||
|
|
||||||
|
if (contact.family_user_id) {
|
||||||
|
return res.status(403).json({ error: 'Familienmitglieder können nicht aus der Kontaktliste gelöscht werden.', code: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const result = db.get().prepare('DELETE FROM contacts WHERE id = ?').run(id);
|
const result = db.get().prepare('DELETE FROM contacts WHERE id = ?').run(id);
|
||||||
if (result.changes === 0)
|
if (result.changes === 0)
|
||||||
return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
|
return res.status(404).json({ error: 'Kontakt nicht gefunden', code: 404 });
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
|
import { MIGRATIONS_SQL } from './server/db-schema-test.js';
|
||||||
|
|
||||||
|
let passed = 0, failed = 0;
|
||||||
|
function test(name, fn) {
|
||||||
|
try { fn(); console.log(` ✓ ${name}`); passed++; }
|
||||||
|
catch (err) { console.error(` ✗ ${name}: ${err.message}`); failed++; }
|
||||||
|
}
|
||||||
|
function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion failed'); }
|
||||||
|
|
||||||
|
const db = new DatabaseSync(':memory:');
|
||||||
|
db.exec('PRAGMA foreign_keys = ON;');
|
||||||
|
|
||||||
|
// Setup schema up to migration 23 (where family_user_id was added)
|
||||||
|
// Since we don't have all migrations in SQL strings easily available,
|
||||||
|
// we'll just mock the necessary tables for this specific test.
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
display_name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
family_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const userId = db.prepare("INSERT INTO users (display_name) VALUES ('Papa')").run().lastInsertRowid;
|
||||||
|
const contactIdFamily = db.prepare("INSERT INTO contacts (name, family_user_id) VALUES ('Papa', ?)").run(userId).lastInsertRowid;
|
||||||
|
const contactIdRegular = db.prepare("INSERT INTO contacts (name, family_user_id) VALUES ('Regular Contact', NULL)").run().lastInsertRowid;
|
||||||
|
|
||||||
|
console.log('\n[Family-Contacts-Test] Backend Deletion Prevention\n');
|
||||||
|
|
||||||
|
test('Regular contact can be deleted', () => {
|
||||||
|
const result = db.prepare('DELETE FROM contacts WHERE id = ?').run(contactIdRegular);
|
||||||
|
assert(result.changes === 1, 'Should have deleted 1 row');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Family contact should NOT be deleted if we apply the logic from the route', () => {
|
||||||
|
// Mocking the logic in server/routes/contacts.js
|
||||||
|
const id = contactIdFamily;
|
||||||
|
const contact = db.prepare('SELECT family_user_id FROM contacts WHERE id = ?').get(id);
|
||||||
|
|
||||||
|
let deleted = false;
|
||||||
|
let forbidden = false;
|
||||||
|
|
||||||
|
if (contact.family_user_id) {
|
||||||
|
forbidden = true;
|
||||||
|
} else {
|
||||||
|
db.prepare('DELETE FROM contacts WHERE id = ?').run(id);
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(forbidden === true, 'Should be forbidden');
|
||||||
|
assert(deleted === false, 'Should not have been deleted');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\nResults: ${passed} passed, ${failed} failed\n`);
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
Reference in New Issue
Block a user