v0.11.11 updated user suspend/delete rules

This commit is contained in:
2026-03-21 18:55:07 -04:00
parent 253bc1f963
commit 9245c6032b
5 changed files with 43 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "jama-backend",
"version": "0.11.10",
"version": "0.11.11",
"description": "TeamChat backend server",
"main": "src/index.js",
"scripts": {

View File

@@ -86,7 +86,7 @@ router.post('/', authMiddleware, adminMiddleware, async (req, res) => {
if (!name || !email) return res.status(400).json({ error: 'Name and email required' });
if (!isValidEmail(email)) return res.status(400).json({ error: 'Invalid email address' });
try {
const exists = await queryOne(req.schema, 'SELECT id FROM users WHERE email = $1', [email]);
const exists = await queryOne(req.schema, "SELECT id FROM users WHERE email = $1 AND status != 'deleted'", [email]);
if (exists) return res.status(400).json({ error: 'Email already in use' });
const resolvedName = await resolveUniqueName(req.schema, name.trim());
const pw = (password || '').trim() || process.env.USER_PASS || 'user@1234';
@@ -120,7 +120,7 @@ router.post('/bulk', authMiddleware, adminMiddleware, async (req, res) => {
if (!isValidEmail(email)) { results.skipped.push({ email, reason: 'Invalid email address' }); continue; }
if (seenEmails.has(email)) { results.skipped.push({ email, reason: 'Duplicate email in CSV' }); continue; }
seenEmails.add(email);
const exists = await queryOne(req.schema, 'SELECT id FROM users WHERE email=$1', [email]);
const exists = await queryOne(req.schema, "SELECT id FROM users WHERE email=$1 AND status != 'deleted'", [email]);
if (exists) { results.skipped.push({ email, reason: 'Email already exists' }); continue; }
try {
const resolvedName = await resolveUniqueName(req.schema, name);
@@ -208,22 +208,48 @@ router.delete('/:id', authMiddleware, adminMiddleware, async (req, res)
if (!t) return res.status(404).json({ error: 'User not found' });
if (t.is_default_admin) return res.status(403).json({ error: 'Cannot delete default admin' });
// Mark deleted
await exec(req.schema, "UPDATE users SET status='deleted', updated_at=NOW() WHERE id=$1", [t.id]);
// ── 1. Anonymise the user record ─────────────────────────────────────────
// Scrub the email immediately so the address is free for re-use.
// Replace name/display_name/avatar/about_me so no PII is retained.
await exec(req.schema, `
UPDATE users SET
status = 'deleted',
email = $1,
name = 'Deleted User',
display_name = NULL,
avatar = NULL,
about_me = NULL,
password = '',
updated_at = NOW()
WHERE id = $2
`, [`deleted_${t.id}@deleted`, t.id]);
// Remove from all chat group memberships
await exec(req.schema, 'DELETE FROM group_members WHERE user_id=$1', [t.id]);
// ── 2. Anonymise their messages ───────────────────────────────────────────
// Mark all their messages as deleted so they render as "This message was
// deleted" in conversation history — no content holes for other members.
await exec(req.schema,
'UPDATE messages SET is_deleted=TRUE, content=NULL, image_url=NULL WHERE user_id=$1 AND is_deleted=FALSE',
[t.id]
);
// Remove from all user groups (managed groups)
// ── 3. Freeze any DMs that only had this user + one other person ──────────
// The surviving peer still has their DM visible but it becomes read-only
// (frozen) since the other party is gone. Group chats (3+ people) are
// left intact — the other members' history and ongoing chat is unaffected.
await exec(req.schema, `
UPDATE groups SET is_readonly=TRUE, updated_at=NOW()
WHERE is_direct=TRUE
AND (direct_peer1_id=$1 OR direct_peer2_id=$1)
`, [t.id]);
// ── 4. Remove memberships ────────────────────────────────────────────────
await exec(req.schema, 'DELETE FROM group_members WHERE user_id=$1', [t.id]);
await exec(req.schema, 'DELETE FROM user_group_members WHERE user_id=$1', [t.id]);
// Clear all active sessions so they cannot log in
await exec(req.schema, 'DELETE FROM active_sessions WHERE user_id=$1', [t.id]);
// Remove push subscriptions
// ── 5. Purge sessions, push subscriptions, notifications ─────────────────
await exec(req.schema, 'DELETE FROM active_sessions WHERE user_id=$1', [t.id]);
await exec(req.schema, 'DELETE FROM push_subscriptions WHERE user_id=$1', [t.id]);
// Remove event availability responses
await exec(req.schema, 'DELETE FROM notifications WHERE user_id=$1', [t.id]);
await exec(req.schema, 'DELETE FROM event_availability WHERE user_id=$1', [t.id]);
res.json({ success: true });

View File

@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
VERSION="${1:-0.11.10}"
VERSION="${1:-0.11.11}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{
"name": "jama-frontend",
"version": "0.11.10",
"version": "0.11.11",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -63,7 +63,7 @@ function UserRow({ u, onUpdated }) {
};
const handleDelete = async () => {
if (u.role === 'admin') return toast('Demote to member before deleting an admin', 'error');
if (!confirm(`Delete ${u.name}? Their messages will remain but they cannot log in.`)) return;
if (!confirm(`Delete ${u.name}?\n\nThis will:\n• Anonymise their account and free their email for re-use\n• Remove all their messages from conversations\n• Freeze any direct messages they were part of\n• Remove all their group memberships\n\nThis cannot be undone.`)) return;
try { await api.deleteUser(u.id); toast('User deleted', 'success'); onUpdated(); }
catch (e) { toast(e.message, 'error'); }
};