From 9ac5b91f6fffb0c1f879acf2cad569e153ce744b Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 17 Aug 2026 19:34:39 +0200 Subject: [PATCH] fix: harden bottom bar timers messages and access --- app/modules/bottom_bar/backend/router.py | 2 +- app/modules/bottom_bar/backend/service.py | 88 ++++++++------ app/modules/sag/frontend/views.py | 12 +- app/shared/frontend/base.html | 2 +- app/timetracking/backend/router.py | 29 +++++ .../231_bottom_bar_message_receipts.sql | 23 ++++ static/js/bottom-bar.js | 110 ++++++++++++++---- 7 files changed, 199 insertions(+), 67 deletions(-) create mode 100644 migrations/231_bottom_bar_message_receipts.sql diff --git a/app/modules/bottom_bar/backend/router.py b/app/modules/bottom_bar/backend/router.py index 98f5dcf..6886d5b 100644 --- a/app/modules/bottom_bar/backend/router.py +++ b/app/modules/bottom_bar/backend/router.py @@ -655,7 +655,7 @@ def _has_boss_access(current_user: dict) -> bool: (int(current_user_id),), ) or [] names = [str(r.get("name") or "") for r in rows] - tokens = ("admin", "manager", "leder", "chef", "teknik", "technician", "support") + tokens = ("admin", "manager", "leder", "chef") return any(any(token in name for token in tokens) for name in names) diff --git a/app/modules/bottom_bar/backend/service.py b/app/modules/bottom_bar/backend/service.py index 6c36099..19de73b 100644 --- a/app/modules/bottom_bar/backend/service.py +++ b/app/modules/bottom_bar/backend/service.py @@ -145,12 +145,16 @@ def get_user_messages_summary(user_id: Optional[int], limit: int = 20) -> Dict[s m.message_text, m.requires_manual_ack, m.created_at, - m.read_at, + receipt.read_at, + receipt.acknowledged_at, COALESCE(NULLIF(sender.full_name, ''), sender.username, ('Bruger #' || sender.user_id::text)) AS sender_name, COALESCE(NULLIF(recipient.full_name, ''), recipient.username, ('Bruger #' || recipient.user_id::text)) AS recipient_name FROM bottom_bar_messages m JOIN users sender ON sender.user_id = m.sender_user_id LEFT JOIN users recipient ON recipient.user_id = m.recipient_user_id + LEFT JOIN bottom_bar_message_receipts receipt + ON receipt.message_id = m.id + AND receipt.user_id = %s WHERE m.sender_user_id = %s OR m.recipient_user_id = %s OR (m.recipient_user_id IS NULL AND EXISTS ( @@ -162,21 +166,24 @@ def get_user_messages_summary(user_id: Optional[int], limit: int = 20) -> Dict[s ORDER BY m.created_at DESC, m.id DESC LIMIT %s """, - (int(user_id), int(user_id), int(user_id), safe_limit), + (int(user_id), int(user_id), int(user_id), int(user_id), safe_limit), ) or [] unread_row = execute_query_single( """ SELECT COUNT(*) AS count - FROM bottom_bar_messages - WHERE read_at IS NULL - AND sender_user_id <> %s + FROM bottom_bar_messages m + LEFT JOIN bottom_bar_message_receipts receipt + ON receipt.message_id = m.id + AND receipt.user_id = %s + WHERE receipt.read_at IS NULL + AND m.sender_user_id <> %s AND ( - recipient_user_id = %s - OR recipient_user_id IS NULL + m.recipient_user_id = %s + OR m.recipient_user_id IS NULL ) """, - (int(user_id), int(user_id)), + (int(user_id), int(user_id), int(user_id)), ) or {} unread_count = _safe_count(unread_row) @@ -197,7 +204,7 @@ def get_user_messages_summary(user_id: Optional[int], limit: int = 20) -> Dict[s "created_at": row.get("created_at").isoformat() if row.get("created_at") else None, "is_own": int(row.get("sender_user_id") or 0) == int(user_id), "is_unread": row.get("read_at") is None and int(row.get("sender_user_id") or 0) != int(user_id), - "is_acknowledged": row.get("read_at") is not None, + "is_acknowledged": row.get("acknowledged_at") is not None, } ) @@ -215,31 +222,40 @@ def mark_user_messages_read(user_id: Optional[int], partner_user_id: Optional[in params: List[Any] = [int(user_id)] if partner_user_id is not None and int(partner_user_id) > 0: where_clause = """ - recipient_user_id = %s - AND read_at IS NULL - AND COALESCE(requires_manual_ack, FALSE) = FALSE - AND sender_user_id <> %s + (m.recipient_user_id = %s OR m.recipient_user_id IS NULL) + AND COALESCE(m.requires_manual_ack, FALSE) = FALSE + AND m.sender_user_id <> %s """ params.append(int(user_id)) - where_clause += " AND sender_user_id = %s" + where_clause += " AND m.sender_user_id = %s" params.append(int(partner_user_id)) else: where_clause = """ - (recipient_user_id = %s OR recipient_user_id IS NULL) - AND read_at IS NULL - AND COALESCE(requires_manual_ack, FALSE) = FALSE - AND sender_user_id <> %s + (m.recipient_user_id = %s OR m.recipient_user_id IS NULL) + AND COALESCE(m.requires_manual_ack, FALSE) = FALSE + AND m.sender_user_id <> %s """ params.append(int(user_id)) row = execute_query_single( f""" - UPDATE bottom_bar_messages - SET read_at = CURRENT_TIMESTAMP - WHERE {where_clause} - RETURNING COUNT(*) OVER() AS affected_count + WITH eligible AS ( + SELECT m.id + FROM bottom_bar_messages m + LEFT JOIN bottom_bar_message_receipts r + ON r.message_id = m.id AND r.user_id = %s + WHERE {where_clause} + AND r.read_at IS NULL + ), updated AS ( + INSERT INTO bottom_bar_message_receipts (message_id, user_id, read_at) + SELECT id, %s, CURRENT_TIMESTAMP FROM eligible + ON CONFLICT (message_id, user_id) DO UPDATE + SET read_at = COALESCE(bottom_bar_message_receipts.read_at, EXCLUDED.read_at) + RETURNING message_id + ) + SELECT COUNT(*) AS affected_count FROM updated """, - tuple(params), + tuple([int(user_id)] + params + [int(user_id)]), ) or {} return int(row.get("affected_count") or 0) @@ -252,14 +268,18 @@ def acknowledge_message(user_id: Optional[int], message_id: int) -> bool: ensure_bottom_bar_messages_schema() row = execute_query_single( """ - UPDATE bottom_bar_messages - SET read_at = CURRENT_TIMESTAMP - WHERE id = %s - AND recipient_user_id = %s - AND read_at IS NULL - RETURNING id + INSERT INTO bottom_bar_message_receipts (message_id, user_id, read_at, acknowledged_at) + SELECT m.id, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + FROM bottom_bar_messages m + WHERE m.id = %s + AND (m.recipient_user_id = %s OR m.recipient_user_id IS NULL) + AND m.sender_user_id <> %s + ON CONFLICT (message_id, user_id) DO UPDATE + SET read_at = COALESCE(bottom_bar_message_receipts.read_at, EXCLUDED.read_at), + acknowledged_at = COALESCE(bottom_bar_message_receipts.acknowledged_at, EXCLUDED.acknowledged_at) + RETURNING message_id AS id """, - (int(message_id), int(user_id)), + (int(user_id), int(message_id), int(user_id), int(user_id)), ) or {} return bool(row.get("id")) @@ -333,19 +353,13 @@ def _can_view_boss_tab(user_id: Optional[int]) -> bool: group_names = _get_user_group_names(user_id) if not group_names: - # Fail-open for authenticated users if group mapping is missing. - return True + return False leadership_tokens = ( "admin", "manager", "leder", "chef", - "teknik", - "technician", - "support", - "drift", - "it", ) return any( any(token in group for token in leadership_tokens) diff --git a/app/modules/sag/frontend/views.py b/app/modules/sag/frontend/views.py index 642b326..1cfb638 100644 --- a/app/modules/sag/frontend/views.py +++ b/app/modules/sag/frontend/views.py @@ -289,8 +289,10 @@ async def sager_liste( placeholders = ", ".join(["%s"] * len(closed_statuses)) query += f" AND LOWER(COALESCE(s.status, '')) NOT IN ({placeholders})" params.extend(closed_statuses) - if normalized_priority: - query += " AND LOWER(COALESCE(s.priority, 'normal')) = %s" + if normalized_priority == "urgent": + query += " AND LOWER(COALESCE(s.priority::text, 'normal')) IN ('urgent', 'high', 'kritisk', 'critical')" + elif normalized_priority: + query += " AND LOWER(COALESCE(s.priority::text, 'normal')) = %s" params.append(normalized_priority) if customer_id_int: query += " AND s.customer_id = %s" @@ -355,8 +357,10 @@ async def sager_liste( placeholders = ", ".join(["%s"] * len(closed_statuses)) fallback_query += f" AND LOWER(COALESCE(s.status, '')) NOT IN ({placeholders})" fallback_params.extend(closed_statuses) - if normalized_priority: - fallback_query += " AND LOWER(COALESCE(s.priority, 'normal')) = %s" + if normalized_priority == "urgent": + fallback_query += " AND LOWER(COALESCE(s.priority::text, 'normal')) IN ('urgent', 'high', 'kritisk', 'critical')" + elif normalized_priority: + fallback_query += " AND LOWER(COALESCE(s.priority::text, 'normal')) = %s" fallback_params.append(normalized_priority) if customer_id_int: fallback_query += " AND s.customer_id = %s" diff --git a/app/shared/frontend/base.html b/app/shared/frontend/base.html index 3889fe7..70d7edc 100644 --- a/app/shared/frontend/base.html +++ b/app/shared/frontend/base.html @@ -1578,7 +1578,7 @@ if (bmcOriginalFetch) { - +