2025-12-06 11:04:19 +01:00
"""
Settings and User Management API Router
"""
from fastapi import APIRouter , HTTPException
from typing import List , Optional , Dict
from pydantic import BaseModel
from app . core . database import execute_query
import logging
logger = logging . getLogger ( __name__ )
router = APIRouter ( )
# Pydantic Models
class Setting ( BaseModel ) :
id : int
key : str
value : Optional [ str ]
category : str
description : Optional [ str ]
value_type : str
is_public : bool
class SettingUpdate ( BaseModel ) :
value : str
class User ( BaseModel ) :
id : int
username : str
email : Optional [ str ]
full_name : Optional [ str ]
is_active : bool
last_login : Optional [ str ]
created_at : str
class UserCreate ( BaseModel ) :
username : str
email : str
password : str
full_name : Optional [ str ] = None
class UserUpdate ( BaseModel ) :
email : Optional [ str ] = None
full_name : Optional [ str ] = None
is_active : Optional [ bool ] = None
# Settings Endpoints
@router.get ( " /settings " , response_model = List [ Setting ] , tags = [ " Settings " ] )
async def get_settings ( category : Optional [ str ] = None ) :
""" Get all settings or filter by category """
query = " SELECT * FROM settings "
params = [ ]
if category :
query + = " WHERE category = %s "
params . append ( category )
query + = " ORDER BY category, key "
result = execute_query ( query , tuple ( params ) if params else None )
return result or [ ]
@router.get ( " /settings/ {key} " , response_model = Setting , tags = [ " Settings " ] )
async def get_setting ( key : str ) :
""" Get a specific setting by key """
query = " SELECT * FROM settings WHERE key = %s "
result = execute_query ( query , ( key , ) )
if not result :
raise HTTPException ( status_code = 404 , detail = " Setting not found " )
return result [ 0 ]
@router.put ( " /settings/ {key} " , response_model = Setting , tags = [ " Settings " ] )
async def update_setting ( key : str , setting : SettingUpdate ) :
""" Update a setting value """
query = """
UPDATE settings
SET value = % s , updated_at = CURRENT_TIMESTAMP
WHERE key = % s
RETURNING *
"""
result = execute_query ( query , ( setting . value , key ) )
if not result :
raise HTTPException ( status_code = 404 , detail = " Setting not found " )
logger . info ( f " ✅ Updated setting: { key } " )
return result [ 0 ]
@router.get ( " /settings/categories/list " , tags = [ " Settings " ] )
async def get_setting_categories ( ) :
""" Get list of all setting categories """
query = " SELECT DISTINCT category FROM settings ORDER BY category "
result = execute_query ( query )
return [ row [ ' category ' ] for row in result ] if result else [ ]
2025-12-22 11:04:09 +01:00
@router.post ( " /settings/sync-from-env " , tags = [ " Settings " ] )
async def sync_settings_from_env ( ) :
""" Sync settings from .env file into database (only updates empty values) """
from app . core . config import settings as env_settings
mapping = {
' vtiger_enabled ' : str ( env_settings . VTIGER_ENABLED ) . lower ( ) ,
' vtiger_url ' : env_settings . VTIGER_URL or ' ' ,
' vtiger_username ' : env_settings . VTIGER_USERNAME or ' ' ,
' economic_enabled ' : str ( env_settings . ECONOMIC_ENABLED ) . lower ( ) ,
' economic_app_secret ' : env_settings . ECONOMIC_APP_SECRET_TOKEN or ' ' ,
' economic_agreement_token ' : env_settings . ECONOMIC_AGREEMENT_GRANT_TOKEN or ' ' ,
}
updated_count = 0
for key , value in mapping . items ( ) :
# Only update if current value is empty or NULL
query = """
UPDATE settings
SET value = % s , updated_at = CURRENT_TIMESTAMP
WHERE key = % s AND ( value IS NULL OR value = ' ' )
RETURNING key
"""
result = execute_query ( query , ( value , key ) )
if result :
updated_count + = 1
logger . info ( f " ✅ Synced { key } from .env " )
return {
" message " : f " Synced { updated_count } settings from .env file " ,
" updated_count " : updated_count
}
2025-12-06 11:04:19 +01:00
# User Management Endpoints
@router.get ( " /users " , response_model = List [ User ] , tags = [ " Users " ] )
async def get_users ( is_active : Optional [ bool ] = None ) :
""" Get all users """
query = " SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users "
params = [ ]
if is_active is not None :
query + = " WHERE is_active = %s "
params . append ( is_active )
query + = " ORDER BY username "
result = execute_query ( query , tuple ( params ) if params else None )
return result or [ ]
@router.get ( " /users/ {user_id} " , response_model = User , tags = [ " Users " ] )
async def get_user ( user_id : int ) :
""" Get user by ID """
query = " SELECT user_id as id, username, email, full_name, is_active, last_login, created_at FROM users WHERE user_id = %s "
result = execute_query ( query , ( user_id , ) )
if not result :
raise HTTPException ( status_code = 404 , detail = " User not found " )
return result [ 0 ]
@router.post ( " /users " , response_model = User , tags = [ " Users " ] )
async def create_user ( user : UserCreate ) :
""" Create a new user """
# Check if username exists
existing = execute_query ( " SELECT user_id FROM users WHERE username = %s " , ( user . username , ) )
if existing :
raise HTTPException ( status_code = 400 , detail = " Username already exists " )
# Hash password (simple SHA256 for now - should use bcrypt in production)
import hashlib
password_hash = hashlib . sha256 ( user . password . encode ( ) ) . hexdigest ( )
query = """
INSERT INTO users ( username , email , password_hash , full_name , is_active )
VALUES ( % s , % s , % s , % s , true )
RETURNING user_id as id , username , email , full_name , is_active , last_login , created_at
"""
result = execute_query ( query , ( user . username , user . email , password_hash , user . full_name ) )
if not result :
raise HTTPException ( status_code = 500 , detail = " Failed to create user " )
logger . info ( f " ✅ Created user: { user . username } " )
return result [ 0 ]
@router.put ( " /users/ {user_id} " , response_model = User , tags = [ " Users " ] )
async def update_user ( user_id : int , user : UserUpdate ) :
""" Update user details """
# Check if user exists
existing = execute_query ( " SELECT user_id FROM users WHERE user_id = %s " , ( user_id , ) )
if not existing :
raise HTTPException ( status_code = 404 , detail = " User not found " )
# Build update query
update_fields = [ ]
params = [ ]
if user . email is not None :
update_fields . append ( " email = %s " )
params . append ( user . email )
if user . full_name is not None :
update_fields . append ( " full_name = %s " )
params . append ( user . full_name )
if user . is_active is not None :
update_fields . append ( " is_active = %s " )
params . append ( user . is_active )
if not update_fields :
raise HTTPException ( status_code = 400 , detail = " No fields to update " )
params . append ( user_id )
query = f """
UPDATE users
SET { ' , ' . join ( update_fields ) } , updated_at = CURRENT_TIMESTAMP
WHERE user_id = % s
RETURNING user_id as id , username , email , full_name , is_active , last_login , created_at
"""
result = execute_query ( query , tuple ( params ) )
if not result :
raise HTTPException ( status_code = 500 , detail = " Failed to update user " )
logger . info ( f " ✅ Updated user: { user_id } " )
return result [ 0 ]
@router.delete ( " /users/ {user_id} " , tags = [ " Users " ] )
async def deactivate_user ( user_id : int ) :
""" Deactivate a user (soft delete) """
query = """
UPDATE users
SET is_active = false , updated_at = CURRENT_TIMESTAMP
WHERE user_id = % s
RETURNING user_id as id
"""
result = execute_query ( query , ( user_id , ) )
if not result :
raise HTTPException ( status_code = 404 , detail = " User not found " )
logger . info ( f " ✅ Deactivated user: { user_id } " )
return { " message " : " User deactivated successfully " }
@router.post ( " /users/ {user_id} /reset-password " , tags = [ " Users " ] )
async def reset_user_password ( user_id : int , new_password : str ) :
""" Reset user password """
import hashlib
password_hash = hashlib . sha256 ( new_password . encode ( ) ) . hexdigest ( )
query = """
UPDATE users
SET password_hash = % s , updated_at = CURRENT_TIMESTAMP
WHERE user_id = % s
RETURNING user_id as id
"""
result = execute_query ( query , ( password_hash , user_id ) )
if not result :
raise HTTPException ( status_code = 404 , detail = " User not found " )
logger . info ( f " ✅ Reset password for user: { user_id } " )
return { " message " : " Password reset successfully " }
2025-12-08 09:15:52 +01:00
2026-01-11 19:23:21 +01:00
# AI Prompts Management
def _get_default_prompts ( ) :
""" Helper to get default system prompts """
2025-12-08 09:15:52 +01:00
from app . services . ollama_service import OllamaService
ollama_service = OllamaService ( )
2026-01-11 19:23:21 +01:00
return {
2025-12-08 09:15:52 +01:00
" invoice_extraction " : {
2026-01-11 19:23:21 +01:00
" name " : " 📄 Faktura Udtrækning (Invoice Parser) " ,
" description " : " System prompt brugt til at udtrække data fra fakturaer og kreditnotaer via Ollama LLM. Håndterer danske nummerformater, datoer og linjegenkendelse. " ,
2025-12-08 09:15:52 +01:00
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : ollama_service . _build_system_prompt ( ) ,
" parameters " : {
" temperature " : 0.1 ,
" top_p " : 0.9 ,
" num_predict " : 2000
}
2026-01-11 19:23:21 +01:00
} ,
" ticket_classification " : {
" name " : " 🎫 Ticket Klassificering (Auto-Triage) " ,
" description " : " Klassificerer indkomne tickets baseret på emne og indhold. Tildeler kategori, prioritet og ansvarlig team. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Du er en erfaren IT-supporter der skal klassificere indkomne support-sager.
Dine opgaver er :
1. Analyser emne og beskrivelse
2. Bestem Kategori : [ Hardware , Software , Netværk , Adgang , Andet ]
3. Bestem Prioritet : [ Lav , Mellem , Høj , Kritisk ]
4. Foreslå handlingsplan ( kort punktform )
Output skal være gyldig JSON :
{
" category " : " string " ,
" priority " : " string " ,
" summary " : " string " ,
" suggested_actions " : [ " string " ]
} """ ,
" parameters " : {
" temperature " : 0.3 ,
" top_p " : 0.95 ,
" num_predict " : 1000
}
} ,
" ticket_summary " : {
" name " : " 📝 Ticket Summering (Fakturagrundlag) " ,
" description " : " Analyserer alle kommentarer og noter i en ticket for at lave et kort, præcist resumé til fakturaen eller kunden. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Du er en administrativ assistent der skal gøre en it-sag klar til fakturering.
Opgave : Læs historikken igennem og skriv et kort resumé af det udførte arbejde .
- Fokusér på løsningen , ikke problemet
- Brug professionelt sprog
- Undlad interne tekniske detaljer ( medmindre relevant for faktura )
- Sprog : Dansk
- Længde : 2 - 3 sætninger
Input : [ Liste af kommentarer ]
Output : [ Fakturastekst ] """ ,
" parameters " : {
" temperature " : 0.2 ,
" top_p " : 0.9 ,
" num_predict " : 500
}
} ,
" kb_generation " : {
" name " : " 📚 Vidensbank Generator (Solution to Article) " ,
" description " : " Omdanner en løst ticket til en generel vejledning til vidensbanken. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Du er teknisk forfatter. Din opgave er at omskrive en konkret support-sag til en generel vejledning.
Regler :
1. Fjern alle kunde - specifikke data ( navne , IP - adresser , passwords )
2. Strukturer som :
- Problem
- Årsag ( hvis kendt )
- Løsning ( Trin - for - trin guide )
3. Brug letforståeligt dansk
4. Formater med Markdown
Input : [ Ticket Beskrivelse + Løsning ]
Output : [ Markdown Guide ] """ ,
" parameters " : {
" temperature " : 0.4 ,
" top_p " : 0.9 ,
" num_predict " : 2000
}
} ,
" troubleshooting_assistant " : {
" name " : " 🔧 Fejlsøgnings Copilot (Tech Helper) " ,
" description " : " Fungerer som en senior-tekniker der giver sparring på en fejlbeskrivelse. Foreslår konkrete fejlsøgningstrin og kommandoer. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Du er en Senior Systemadministrator med 20 års erfaring.
En junior - tekniker spørger om hjælp til et problem .
Din opgave :
1. Analyser symptomerne
2. List de 3 mest sandsynlige årsager
3. Foreslå en trin - for - trin fejlsøgningsplan ( start med det mest sandsynlige )
4. Nævn relevante værktøjer eller kommandoer ( Windows / Linux / Network )
Vær kortfattet , teknisk præcis og handlingsorienteret .
Sprog : Dansk ( men engelske fagtermer er OK ) .
Input : [ Fejlbeskrivelse ]
Output : [ Markdown Guide ] """ ,
" parameters " : {
" temperature " : 0.3 ,
" top_p " : 0.9 ,
" num_predict " : 1500
}
} ,
" sentiment_analysis " : {
" name " : " 🌡️ Sentiment Analyse (Kunde-Humør) " ,
" description " : " Analyserer tonen i en kundehenvendelse for at vurdere hast, frustration og risiko. Bruges til prioritering. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Analyser tonen i følgende tekst fra en kunde.
Bestem følgende :
1. Sentiment : [ Positiv , Neutral , Frustreret , Vred ]
2. Hastegrad - indikatorer : Er der ord der indikerer panik eller kritisk hast ?
3. Risikovurdering ( 0 - 10 ) : Hvor stor risiko er der for at kunden forlader os ? ( 10 = Høj )
Returner resultatet som JSON format .
Input : [ Kunde Tekst ]
Output : { " sentiment " : " ... " , " urgency " : " ... " , " risk_score " : 0 } """ ,
" parameters " : {
" temperature " : 0.1 ,
" top_p " : 0.9 ,
" num_predict " : 500
}
} ,
" meeting_action_items " : {
" name " : " 📋 Mødenoter til Opgaver (Action Extraction) " ,
" description " : " Scanner rå mødereferater eller notater og udtrækker konkrete ' Action Items ' , deadlines og ansvarlige personer. " ,
" model " : ollama_service . model ,
" endpoint " : ollama_service . endpoint ,
" prompt " : """ Du er en effektiv projektleder-assistent.
Din opgave er at scanne mødereferater og udtrække " Action Items " .
For hver opgave skal du finde :
- Aktivitet ( Hvad skal gøres ? )
- Ansvarlig ( Hvem ? )
- Deadline ( Hvornår ? Hvis nævnt )
Ignorer løs snak og diskussioner . Fokusér kun på beslutninger og opgaver der skal udføres .
Outputtet skal være en punktopstilling .
Input : [ Mødenoter ]
Output : [ Liste af opgaver ] """ ,
" parameters " : {
" temperature " : 0.2 ,
" top_p " : 0.9 ,
" num_predict " : 1000
}
2025-12-08 09:15:52 +01:00
}
}
2026-01-11 19:23:21 +01:00
@router.get ( " /ai-prompts " , tags = [ " Settings " ] )
async def get_ai_prompts ( ) :
""" Get all AI prompts (defaults merged with custom overrides) """
prompts = _get_default_prompts ( )
try :
# Check for custom overrides in DB
# Note: Table ai_prompts must rely on migration 066
rows = execute_query ( " SELECT key, prompt_text FROM ai_prompts " )
if rows :
for row in rows :
if row [ ' key ' ] in prompts :
prompts [ row [ ' key ' ] ] [ ' prompt ' ] = row [ ' prompt_text ' ]
prompts [ row [ ' key ' ] ] [ ' is_custom ' ] = True
except Exception as e :
logger . warning ( f " Could not load custom ai prompts: { e } " )
2025-12-08 09:15:52 +01:00
return prompts
2026-01-11 19:23:21 +01:00
class PromptUpdate ( BaseModel ) :
prompt_text : str
@router.put ( " /ai-prompts/ {key} " , tags = [ " Settings " ] )
async def update_ai_prompt ( key : str , update : PromptUpdate ) :
""" Override a system prompt with a custom one """
defaults = _get_default_prompts ( )
if key not in defaults :
raise HTTPException ( status_code = 404 , detail = " Unknown prompt key " )
try :
# Upsert
query = """
INSERT INTO ai_prompts ( key , prompt_text , updated_at )
VALUES ( % s , % s , CURRENT_TIMESTAMP )
ON CONFLICT ( key )
DO UPDATE SET prompt_text = EXCLUDED . prompt_text , updated_at = CURRENT_TIMESTAMP
RETURNING key
"""
execute_query ( query , ( key , update . prompt_text ) )
return { " message " : " Prompt updated " , " key " : key }
except Exception as e :
logger . error ( f " Error saving prompt: { e } " )
raise HTTPException ( status_code = 500 , detail = " Could not save prompt " )
@router.delete ( " /ai-prompts/ {key} " , tags = [ " Settings " ] )
async def reset_ai_prompt ( key : str ) :
""" Reset a prompt to its system default """
try :
execute_query ( " DELETE FROM ai_prompts WHERE key = %s " , ( key , ) )
return { " message " : " Prompt reset to default " }
except Exception as e :
logger . error ( f " Error resetting prompt: { e } " )
raise HTTPException ( status_code = 500 , detail = " Could not reset prompt " )