Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from route_backend_public_workspaces import *
from route_backend_public_documents import *
from route_backend_public_prompts import *
from route_backend_user_agreement import register_route_backend_user_agreement
from route_backend_speech import register_route_backend_speech
from route_backend_tts import register_route_backend_tts
from route_enhanced_citations import register_enhanced_citations_routes
Expand Down Expand Up @@ -617,6 +618,9 @@ def list_semantic_kernel_plugins():
# ------------------- API Public Prompts Routes ----------
register_route_backend_public_prompts(app)

# ------------------- API User Agreement Routes ----------
register_route_backend_user_agreement(app)

# ------------------- Extenral Health Routes ----------
register_route_external_health(app)

Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.235.025"
VERSION = "0.236.007"


SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
Expand Down
135 changes: 135 additions & 0 deletions application/single_app/functions_activity_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,3 +1080,138 @@ def log_public_workspace_status_change(
level=logging.ERROR
)
debug_print(f"⚠️ Warning: Failed to log public workspace status change: {str(e)}")


def log_user_agreement_accepted(
user_id: str,
workspace_type: str,
workspace_id: str,
workspace_name: Optional[str] = None,
action_context: Optional[str] = None
) -> None:
"""
Log when a user accepts a user agreement in a workspace.
This record is used to track acceptance and support daily acceptance features.

Args:
user_id (str): The ID of the user who accepted the agreement
workspace_type (str): Type of workspace ('personal', 'group', 'public')
workspace_id (str): The ID of the workspace
workspace_name (str, optional): The name of the workspace
action_context (str, optional): The context/action that triggered the agreement
(e.g., 'file_upload', 'chat')
"""

try:
import uuid

# Create user agreement acceptance record
acceptance_record = {
'id': str(uuid.uuid4()),
'user_id': user_id,
'activity_type': 'user_agreement_accepted',
'timestamp': datetime.utcnow().isoformat(),
'created_at': datetime.utcnow().isoformat(),
'accepted_date': datetime.utcnow().strftime('%Y-%m-%d'), # Date only for daily lookup
'workspace_type': workspace_type,
'workspace_context': {
f'{workspace_type}_workspace_id': workspace_id,
'workspace_name': workspace_name
},
'action_context': action_context
}

# Save to activity_logs container
cosmos_activity_logs_container.create_item(body=acceptance_record)

# Also log to Application Insights for monitoring
log_event(
message=f"User agreement accepted: user {user_id} in {workspace_type} workspace {workspace_id}",
extra=acceptance_record,
level=logging.INFO
)

debug_print(f"✅ Logged user agreement acceptance: user {user_id} in {workspace_type} workspace {workspace_id}")

except Exception as e:
# Log error but don't fail the operation
log_event(
message=f"Error logging user agreement acceptance: {str(e)}",
extra={
'user_id': user_id,
'workspace_type': workspace_type,
'workspace_id': workspace_id,
'error': str(e)
},
level=logging.ERROR
)
debug_print(f"⚠️ Warning: Failed to log user agreement acceptance: {str(e)}")


def has_user_accepted_agreement_today(
user_id: str,
workspace_type: str,
workspace_id: str
) -> bool:
"""
Check if a user has already accepted the user agreement today for a given workspace.
Used to implement the "accept once per day" feature.

Args:
user_id (str): The ID of the user
workspace_type (str): Type of workspace ('personal', 'group', 'public')
workspace_id (str): The ID of the workspace

Returns:
bool: True if user has accepted today, False otherwise
"""

try:
today_date = datetime.utcnow().strftime('%Y-%m-%d')

# Query for today's acceptance record
query = """
SELECT VALUE COUNT(1) FROM c
WHERE c.user_id = @user_id
AND c.activity_type = 'user_agreement_accepted'
AND c.accepted_date = @today_date
AND c.workspace_type = @workspace_type
AND c.workspace_context[@workspace_id_key] = @workspace_id
"""

workspace_id_key = f'{workspace_type}_workspace_id'

params = [
{"name": "@user_id", "value": user_id},
{"name": "@today_date", "value": today_date},
{"name": "@workspace_type", "value": workspace_type},
{"name": "@workspace_id_key", "value": workspace_id_key},
{"name": "@workspace_id", "value": workspace_id}
]

results = list(cosmos_activity_logs_container.query_items(
query=query,
parameters=params,
enable_cross_partition_query=False # Query by partition key (user_id)
))

count = results[0] if results else 0

debug_print(f"🔍 User agreement check: user {user_id}, workspace {workspace_id}, today={today_date}, accepted={count > 0}")

return count > 0

except Exception as e:
# Log error and return False (require re-acceptance on error)
log_event(
message=f"Error checking user agreement acceptance: {str(e)}",
extra={
'user_id': user_id,
'workspace_type': workspace_type,
'workspace_id': workspace_id,
'error': str(e)
},
level=logging.ERROR
)
debug_print(f"⚠️ Error checking user agreement acceptance: {str(e)}")
return False
167 changes: 167 additions & 0 deletions application/single_app/route_backend_user_agreement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# route_backend_user_agreement.py

from config import *
from functions_authentication import *
from functions_settings import get_settings
from functions_public_workspaces import find_public_workspace_by_id
from functions_activity_logging import log_user_agreement_accepted, has_user_accepted_agreement_today
from swagger_wrapper import swagger_route, get_auth_security
from functions_debug import debug_print


def register_route_backend_user_agreement(app):
"""
Register user agreement API endpoints under '/api/user_agreement/...'
These endpoints handle checking and recording user agreement acceptance.
"""

@app.route("/api/user_agreement/check", methods=["GET"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_check_user_agreement():
"""
GET /api/user_agreement/check
Check if the current user needs to accept a user agreement for a workspace.

Query params:
workspace_id: The workspace ID
workspace_type: The workspace type ('personal', 'group', 'public', 'chat')
action_context: The action context ('file_upload', 'chat') - optional

Returns:
{
needsAgreement: bool,
agreementText: str (if needs agreement),
enableDailyAcceptance: bool
}
"""
info = get_current_user_info()
user_id = info["userId"]

workspace_id = request.args.get("workspace_id")
workspace_type = request.args.get("workspace_type")
action_context = request.args.get("action_context", "file_upload")

if not workspace_id or not workspace_type:
return jsonify({"error": "workspace_id and workspace_type are required"}), 400

# Validate workspace type
valid_types = ["personal", "group", "public", "chat"]
if workspace_type not in valid_types:
return jsonify({"error": f"Invalid workspace_type. Must be one of: {', '.join(valid_types)}"}), 400

# Get global user agreement settings from app settings
settings = get_settings()

# Check if user agreement is enabled globally
if not settings.get("enable_user_agreement", False):
return jsonify({
"needsAgreement": False,
"agreementText": "",
"enableDailyAcceptance": False
}), 200

apply_to = settings.get("user_agreement_apply_to", [])

# Check if the agreement applies to this workspace type or action
applies = False
if workspace_type in apply_to:
applies = True
elif action_context == "chat" and "chat" in apply_to:
applies = True

if not applies:
return jsonify({
"needsAgreement": False,
"agreementText": "",
"enableDailyAcceptance": False
}), 200

# Check if daily acceptance is enabled and user already accepted today
enable_daily_acceptance = settings.get("enable_user_agreement_daily", False)

if enable_daily_acceptance:
already_accepted = has_user_accepted_agreement_today(user_id, workspace_type, workspace_id)
if already_accepted:
debug_print(f"[USER_AGREEMENT] User {user_id} already accepted today for {workspace_type} workspace {workspace_id}")
return jsonify({
"needsAgreement": False,
"agreementText": "",
"enableDailyAcceptance": True,
"alreadyAcceptedToday": True
}), 200

# User needs to accept the agreement
return jsonify({
"needsAgreement": True,
"agreementText": settings.get("user_agreement_text", ""),
"enableDailyAcceptance": enable_daily_acceptance
}), 200

@app.route("/api/user_agreement/accept", methods=["POST"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def api_accept_user_agreement():
"""
POST /api/user_agreement/accept
Record that a user has accepted the user agreement for a workspace.

Body JSON:
{
workspace_id: str,
workspace_type: str ('personal', 'group', 'public'),
action_context: str (optional, e.g., 'file_upload', 'chat')
}

Returns:
{ success: bool, message: str }
"""
info = get_current_user_info()
user_id = info["userId"]

data = request.get_json() or {}
workspace_id = data.get("workspace_id")
workspace_type = data.get("workspace_type")
action_context = data.get("action_context", "file_upload")

if not workspace_id or not workspace_type:
return jsonify({"error": "workspace_id and workspace_type are required"}), 400

# Validate workspace type
valid_types = ["personal", "group", "public"]
if workspace_type not in valid_types:
return jsonify({"error": f"Invalid workspace_type. Must be one of: {', '.join(valid_types)}"}), 400

# Get workspace name for logging
workspace_name = None
if workspace_type == "public":
ws = find_public_workspace_by_id(workspace_id)
if ws:
workspace_name = ws.get("name", "")

# Log the acceptance
try:
log_user_agreement_accepted(
user_id=user_id,
workspace_type=workspace_type,
workspace_id=workspace_id,
workspace_name=workspace_name,
action_context=action_context
)

debug_print(f"[USER_AGREEMENT] Recorded acceptance: user {user_id}, {workspace_type} workspace {workspace_id}")

return jsonify({
"success": True,
"message": "User agreement acceptance recorded"
}), 200

except Exception as e:
debug_print(f"[USER_AGREEMENT] Error recording acceptance: {str(e)}")
log_event(f"Error recording user agreement acceptance: {str(e)}", level=logging.ERROR)
return jsonify({
"success": False,
"error": f"Failed to record acceptance: {str(e)}"
}), 500
Loading