bmc_hub/app/services/anydesk_links.py

146 lines
9.2 KiB
Python

"""Remember the customer end of an AnyDesk session, never the technician end."""
import json
import re
from fastapi import HTTPException
from app.core.database import execute_query
def customer_machine_id(device_info):
if isinstance(device_info, str):
device_info = json.loads(device_info)
value = re.sub(r'[\s-]', '', str((device_info or {}).get('to_id') or ''))
return value if re.fullmatch(r'[0-9]{1,64}', value) else None
def remember_machine(session_id, customer_id, contact_id, hardware_asset_id=None):
"""Caller owns the transaction including the selected session's update."""
rows = execute_query('SELECT device_info,hardware_asset_id FROM anydesk_sessions WHERE id=%s', (session_id,))
if not rows:
raise HTTPException(404, 'Sessionen findes ikke')
machine_id = customer_machine_id(rows[0]['device_info'])
if not machine_id:
raise HTTPException(409, 'Sessionen mangler et entydigt AnyDesk-ID for kundens maskine')
customer = execute_query('SELECT id FROM customers WHERE id=%s AND is_active=TRUE', (customer_id,))
if not customer:
raise HTTPException(409, 'Vælg et aktivt firma for at gemme AnyDesk-koblingen')
if contact_id is not None:
contact = execute_query('''SELECT c.id FROM contacts c
JOIN contact_companies cc ON cc.contact_id=c.id
WHERE c.id=%s AND cc.customer_id=%s AND c.is_active=TRUE''', (contact_id, customer_id))
if not contact:
raise HTTPException(409, 'Kontaktpersonen skal være aktiv og tilknyttet det valgte firma')
hardware_id = _remember_hardware(machine_id, customer_id, contact_id,
hardware_asset_id or rows[0].get('hardware_asset_id'))
execute_query('UPDATE anydesk_sessions SET hardware_asset_id=%s WHERE id=%s', (hardware_id, session_id))
return machine_id
def _remember_hardware(machine_id, customer_id, contact_id, selected_id):
# Serialize registrations of this ID so repeated/concurrent saves reuse one PC.
execute_query("SELECT pg_advisory_xact_lock(hashtextextended(%s,0))", ('anydesk-hardware:' + machine_id,))
matches = execute_query('''SELECT id,anydesk_id,current_owner_customer_id,current_owner_type
FROM hardware_assets WHERE deleted_at IS NULL
AND regexp_replace(COALESCE(anydesk_id,''),'[[:space:]-]','','g')=%s FOR UPDATE''', (machine_id,))
if len(matches) > 1:
raise HTTPException(409, 'AnyDesk-ID findes på flere hardwareposter. Ret dubletterne før registrering.')
if selected_id is not None:
selected = execute_query('''SELECT id,anydesk_id,current_owner_customer_id,current_owner_type
FROM hardware_assets WHERE id=%s AND deleted_at IS NULL FOR UPDATE''', (selected_id,))
if not selected:
raise HTTPException(409, 'Den valgte hardware findes ikke')
if matches and matches[0]['id'] != selected_id:
raise HTTPException(409, 'AnyDesk-ID findes allerede på en anden hardwarepost')
if selected[0].get('anydesk_id') and customer_machine_id({'to_id': selected[0]['anydesk_id']}) != machine_id:
raise HTTPException(409, 'Den valgte hardware har et andet AnyDesk-ID')
matches = selected
elif not matches and contact_id is not None:
matches = execute_query('''SELECT h.id,h.anydesk_id,h.current_owner_customer_id,h.current_owner_type
FROM hardware_assets h JOIN hardware_contacts hc ON hc.hardware_id=h.id
WHERE hc.contact_id=%s AND h.current_owner_customer_id=%s AND h.deleted_at IS NULL
AND h.status='active' AND h.asset_type IN ('pc','laptop')
AND NULLIF(BTRIM(h.anydesk_id),'') IS NULL FOR UPDATE OF h''', (contact_id, customer_id))
if len(matches) > 1:
raise HTTPException(409, 'Kontaktpersonen har flere computere uden AnyDesk-ID. Vælg den rigtige hardware først.')
if matches:
hardware = matches[0]
if hardware['current_owner_customer_id'] not in (None, customer_id):
raise HTTPException(409, 'Hardwaren tilhører et andet firma. Ret ejerskabet på hardwarekortet først.')
if hardware['current_owner_customer_id'] is None and hardware.get('current_owner_type') not in (None, 'customer'):
raise HTTPException(409, 'Hardwarens ejerskab skal afklares på hardwarekortet først.')
if contact_id is not None:
other_contacts = execute_query('''SELECT hc.contact_id FROM hardware_contacts hc
JOIN contacts c ON c.id=hc.contact_id AND c.is_active=TRUE
WHERE hc.hardware_id=%s AND hc.contact_id<>%s''', (hardware['id'], contact_id))
if other_contacts:
raise HTTPException(409, 'Hardwaren er koblet til en anden kontaktperson. Ret koblingen på hardwarekortet først.')
hardware_id = hardware['id']
execute_query('''UPDATE hardware_assets SET anydesk_id=%s,anydesk_link=%s,
current_owner_type=COALESCE(current_owner_type,'customer'),current_owner_customer_id=%s,updated_at=NOW() WHERE id=%s''',
(machine_id, 'anydesk:' + machine_id, customer_id, hardware_id))
else:
hardware_id = execute_query('''INSERT INTO hardware_assets
(asset_type,model,current_owner_type,current_owner_customer_id,status,anydesk_id,anydesk_link,notes)
VALUES('pc','PC','customer',%s,'active',%s,%s,'Oprettet fra AnyDesk-session') RETURNING id''',
(customer_id, machine_id, 'anydesk:' + machine_id))[0]['id']
if not matches or matches[0]['current_owner_customer_id'] is None:
execute_query('''INSERT INTO hardware_ownership_history
(hardware_id,owner_type,owner_customer_id,start_date,notes)
VALUES(%s,'customer',%s,CURRENT_DATE,'Registreret fra AnyDesk-session')''', (hardware_id, customer_id))
if contact_id is not None:
execute_query('''INSERT INTO hardware_contacts(hardware_id,contact_id,role,source)
VALUES(%s,%s,'primary','anydesk') ON CONFLICT(hardware_id,contact_id) DO NOTHING''',
(hardware_id, contact_id))
return hardware_id
def apply_machine_links(machine_id=None):
"""Hardware is authoritative. Ambiguous machine IDs are never auto-linked."""
rows = execute_query('''UPDATE anydesk_sessions s SET hardware_asset_id=COALESCE(s.hardware_asset_id,m.id),
customer_id=COALESCE(s.customer_id,m.customer_id),contact_id=COALESCE(s.contact_id,m.contact_id),updated_at=NOW()
FROM (WITH hardware AS (
SELECT regexp_replace(anydesk_id,'[[:space:]-]','','g') AS machine,
MIN(id) AS id
FROM hardware_assets WHERE deleted_at IS NULL AND anydesk_id IS NOT NULL
GROUP BY regexp_replace(anydesk_id,'[[:space:]-]','','g') HAVING COUNT(*)=1
), links AS (
SELECT h.id,h.current_owner_customer_id AS customer_id,hw.machine,
CASE WHEN COUNT(DISTINCT con.id)=1 THEN MIN(con.id) ELSE NULL END AS contact_id
FROM hardware hw JOIN hardware_assets h ON h.id=hw.id
JOIN customers c ON c.id=h.current_owner_customer_id AND c.is_active=TRUE
LEFT JOIN hardware_contacts hc ON hc.hardware_id=h.id
LEFT JOIN contacts con ON con.id=hc.contact_id AND con.is_active=TRUE AND EXISTS (
SELECT 1 FROM contact_companies cc WHERE cc.contact_id=con.id AND cc.customer_id=c.id)
WHERE hw.machine ~ '^[0-9]+$' AND (%s IS NULL OR hw.machine=%s)
GROUP BY h.id,h.current_owner_customer_id,hw.machine
) SELECT * FROM links) m
WHERE regexp_replace(COALESCE(s.device_info->>'to_id',''),'[[:space:]-]','','g')=m.machine
AND (s.hardware_asset_id IS NULL OR s.hardware_asset_id=m.id)
AND ((s.customer_id IS NULL AND s.contact_id IS NULL) OR s.customer_id=m.customer_id)
AND (s.hardware_asset_id IS NULL OR s.customer_id IS NULL OR (s.contact_id IS NULL AND m.contact_id IS NOT NULL))
RETURNING s.id''', (machine_id, machine_id))
return len(rows or []) + _apply_legacy_links(machine_id)
def _apply_legacy_links(machine_id=None):
"""Fill only unassigned sessions or a missing contact for the same customer."""
table = execute_query("SELECT to_regclass('anydesk_machine_links') AS present")
if not table or not table[0]['present']:
return 0
rows = execute_query('''UPDATE anydesk_sessions s SET
customer_id=COALESCE(s.customer_id,m.customer_id),
contact_id=COALESCE(s.contact_id,m.contact_id),updated_at=NOW()
FROM anydesk_machine_links m JOIN customers c ON c.id=m.customer_id AND c.is_active=TRUE
LEFT JOIN contacts con ON con.id=m.contact_id
WHERE regexp_replace(COALESCE(s.device_info->>'to_id',''), '[[:space:]-]', '', 'g')=m.anydesk_id
AND (%s IS NULL OR m.anydesk_id=%s)
AND NOT EXISTS (SELECT 1 FROM hardware_assets h WHERE h.deleted_at IS NULL
AND regexp_replace(COALESCE(h.anydesk_id,''),'[[:space:]-]','','g')=m.anydesk_id)
AND (m.contact_id IS NULL OR (con.is_active=TRUE AND EXISTS (
SELECT 1 FROM contact_companies cc WHERE cc.contact_id=m.contact_id AND cc.customer_id=m.customer_id)))
AND ((s.customer_id IS NULL AND s.contact_id IS NULL)
OR (s.customer_id=m.customer_id AND s.contact_id IS NULL AND m.contact_id IS NOT NULL))
RETURNING s.id''', (machine_id, machine_id))
return len(rows or [])