Skip to content

Commit 467d7b9

Browse files
authored
Configure a user agreement that users must accept before uploading files (#620)
* Configure a user agreement that users must accept before uploading files * added log_event to exceptions
1 parent db5a3f8 commit 467d7b9

16 files changed

Lines changed: 1240 additions & 26 deletions

application/single_app/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from route_backend_public_workspaces import *
5959
from route_backend_public_documents import *
6060
from route_backend_public_prompts import *
61+
from route_backend_user_agreement import register_route_backend_user_agreement
6162
from route_backend_speech import register_route_backend_speech
6263
from route_backend_tts import register_route_backend_tts
6364
from route_enhanced_citations import register_enhanced_citations_routes
@@ -617,6 +618,9 @@ def list_semantic_kernel_plugins():
617618
# ------------------- API Public Prompts Routes ----------
618619
register_route_backend_public_prompts(app)
619620

621+
# ------------------- API User Agreement Routes ----------
622+
register_route_backend_user_agreement(app)
623+
620624
# ------------------- Extenral Health Routes ----------
621625
register_route_external_health(app)
622626

application/single_app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
EXECUTOR_TYPE = 'thread'
8989
EXECUTOR_MAX_WORKERS = 30
9090
SESSION_TYPE = 'filesystem'
91-
VERSION = "0.235.025"
91+
VERSION = "0.236.007"
9292

9393

9494
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')

application/single_app/functions_activity_logging.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,3 +1080,138 @@ def log_public_workspace_status_change(
10801080
level=logging.ERROR
10811081
)
10821082
debug_print(f"⚠️ Warning: Failed to log public workspace status change: {str(e)}")
1083+
1084+
1085+
def log_user_agreement_accepted(
1086+
user_id: str,
1087+
workspace_type: str,
1088+
workspace_id: str,
1089+
workspace_name: Optional[str] = None,
1090+
action_context: Optional[str] = None
1091+
) -> None:
1092+
"""
1093+
Log when a user accepts a user agreement in a workspace.
1094+
This record is used to track acceptance and support daily acceptance features.
1095+
1096+
Args:
1097+
user_id (str): The ID of the user who accepted the agreement
1098+
workspace_type (str): Type of workspace ('personal', 'group', 'public')
1099+
workspace_id (str): The ID of the workspace
1100+
workspace_name (str, optional): The name of the workspace
1101+
action_context (str, optional): The context/action that triggered the agreement
1102+
(e.g., 'file_upload', 'chat')
1103+
"""
1104+
1105+
try:
1106+
import uuid
1107+
1108+
# Create user agreement acceptance record
1109+
acceptance_record = {
1110+
'id': str(uuid.uuid4()),
1111+
'user_id': user_id,
1112+
'activity_type': 'user_agreement_accepted',
1113+
'timestamp': datetime.utcnow().isoformat(),
1114+
'created_at': datetime.utcnow().isoformat(),
1115+
'accepted_date': datetime.utcnow().strftime('%Y-%m-%d'), # Date only for daily lookup
1116+
'workspace_type': workspace_type,
1117+
'workspace_context': {
1118+
f'{workspace_type}_workspace_id': workspace_id,
1119+
'workspace_name': workspace_name
1120+
},
1121+
'action_context': action_context
1122+
}
1123+
1124+
# Save to activity_logs container
1125+
cosmos_activity_logs_container.create_item(body=acceptance_record)
1126+
1127+
# Also log to Application Insights for monitoring
1128+
log_event(
1129+
message=f"User agreement accepted: user {user_id} in {workspace_type} workspace {workspace_id}",
1130+
extra=acceptance_record,
1131+
level=logging.INFO
1132+
)
1133+
1134+
debug_print(f"✅ Logged user agreement acceptance: user {user_id} in {workspace_type} workspace {workspace_id}")
1135+
1136+
except Exception as e:
1137+
# Log error but don't fail the operation
1138+
log_event(
1139+
message=f"Error logging user agreement acceptance: {str(e)}",
1140+
extra={
1141+
'user_id': user_id,
1142+
'workspace_type': workspace_type,
1143+
'workspace_id': workspace_id,
1144+
'error': str(e)
1145+
},
1146+
level=logging.ERROR
1147+
)
1148+
debug_print(f"⚠️ Warning: Failed to log user agreement acceptance: {str(e)}")
1149+
1150+
1151+
def has_user_accepted_agreement_today(
1152+
user_id: str,
1153+
workspace_type: str,
1154+
workspace_id: str
1155+
) -> bool:
1156+
"""
1157+
Check if a user has already accepted the user agreement today for a given workspace.
1158+
Used to implement the "accept once per day" feature.
1159+
1160+
Args:
1161+
user_id (str): The ID of the user
1162+
workspace_type (str): Type of workspace ('personal', 'group', 'public')
1163+
workspace_id (str): The ID of the workspace
1164+
1165+
Returns:
1166+
bool: True if user has accepted today, False otherwise
1167+
"""
1168+
1169+
try:
1170+
today_date = datetime.utcnow().strftime('%Y-%m-%d')
1171+
1172+
# Query for today's acceptance record
1173+
query = """
1174+
SELECT VALUE COUNT(1) FROM c
1175+
WHERE c.user_id = @user_id
1176+
AND c.activity_type = 'user_agreement_accepted'
1177+
AND c.accepted_date = @today_date
1178+
AND c.workspace_type = @workspace_type
1179+
AND c.workspace_context[@workspace_id_key] = @workspace_id
1180+
"""
1181+
1182+
workspace_id_key = f'{workspace_type}_workspace_id'
1183+
1184+
params = [
1185+
{"name": "@user_id", "value": user_id},
1186+
{"name": "@today_date", "value": today_date},
1187+
{"name": "@workspace_type", "value": workspace_type},
1188+
{"name": "@workspace_id_key", "value": workspace_id_key},
1189+
{"name": "@workspace_id", "value": workspace_id}
1190+
]
1191+
1192+
results = list(cosmos_activity_logs_container.query_items(
1193+
query=query,
1194+
parameters=params,
1195+
enable_cross_partition_query=False # Query by partition key (user_id)
1196+
))
1197+
1198+
count = results[0] if results else 0
1199+
1200+
debug_print(f"🔍 User agreement check: user {user_id}, workspace {workspace_id}, today={today_date}, accepted={count > 0}")
1201+
1202+
return count > 0
1203+
1204+
except Exception as e:
1205+
# Log error and return False (require re-acceptance on error)
1206+
log_event(
1207+
message=f"Error checking user agreement acceptance: {str(e)}",
1208+
extra={
1209+
'user_id': user_id,
1210+
'workspace_type': workspace_type,
1211+
'workspace_id': workspace_id,
1212+
'error': str(e)
1213+
},
1214+
level=logging.ERROR
1215+
)
1216+
debug_print(f"⚠️ Error checking user agreement acceptance: {str(e)}")
1217+
return False
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# route_backend_user_agreement.py
2+
3+
from config import *
4+
from functions_authentication import *
5+
from functions_settings import get_settings
6+
from functions_public_workspaces import find_public_workspace_by_id
7+
from functions_activity_logging import log_user_agreement_accepted, has_user_accepted_agreement_today
8+
from swagger_wrapper import swagger_route, get_auth_security
9+
from functions_debug import debug_print
10+
11+
12+
def register_route_backend_user_agreement(app):
13+
"""
14+
Register user agreement API endpoints under '/api/user_agreement/...'
15+
These endpoints handle checking and recording user agreement acceptance.
16+
"""
17+
18+
@app.route("/api/user_agreement/check", methods=["GET"])
19+
@swagger_route(security=get_auth_security())
20+
@login_required
21+
@user_required
22+
def api_check_user_agreement():
23+
"""
24+
GET /api/user_agreement/check
25+
Check if the current user needs to accept a user agreement for a workspace.
26+
27+
Query params:
28+
workspace_id: The workspace ID
29+
workspace_type: The workspace type ('personal', 'group', 'public', 'chat')
30+
action_context: The action context ('file_upload', 'chat') - optional
31+
32+
Returns:
33+
{
34+
needsAgreement: bool,
35+
agreementText: str (if needs agreement),
36+
enableDailyAcceptance: bool
37+
}
38+
"""
39+
info = get_current_user_info()
40+
user_id = info["userId"]
41+
42+
workspace_id = request.args.get("workspace_id")
43+
workspace_type = request.args.get("workspace_type")
44+
action_context = request.args.get("action_context", "file_upload")
45+
46+
if not workspace_id or not workspace_type:
47+
return jsonify({"error": "workspace_id and workspace_type are required"}), 400
48+
49+
# Validate workspace type
50+
valid_types = ["personal", "group", "public", "chat"]
51+
if workspace_type not in valid_types:
52+
return jsonify({"error": f"Invalid workspace_type. Must be one of: {', '.join(valid_types)}"}), 400
53+
54+
# Get global user agreement settings from app settings
55+
settings = get_settings()
56+
57+
# Check if user agreement is enabled globally
58+
if not settings.get("enable_user_agreement", False):
59+
return jsonify({
60+
"needsAgreement": False,
61+
"agreementText": "",
62+
"enableDailyAcceptance": False
63+
}), 200
64+
65+
apply_to = settings.get("user_agreement_apply_to", [])
66+
67+
# Check if the agreement applies to this workspace type or action
68+
applies = False
69+
if workspace_type in apply_to:
70+
applies = True
71+
elif action_context == "chat" and "chat" in apply_to:
72+
applies = True
73+
74+
if not applies:
75+
return jsonify({
76+
"needsAgreement": False,
77+
"agreementText": "",
78+
"enableDailyAcceptance": False
79+
}), 200
80+
81+
# Check if daily acceptance is enabled and user already accepted today
82+
enable_daily_acceptance = settings.get("enable_user_agreement_daily", False)
83+
84+
if enable_daily_acceptance:
85+
already_accepted = has_user_accepted_agreement_today(user_id, workspace_type, workspace_id)
86+
if already_accepted:
87+
debug_print(f"[USER_AGREEMENT] User {user_id} already accepted today for {workspace_type} workspace {workspace_id}")
88+
return jsonify({
89+
"needsAgreement": False,
90+
"agreementText": "",
91+
"enableDailyAcceptance": True,
92+
"alreadyAcceptedToday": True
93+
}), 200
94+
95+
# User needs to accept the agreement
96+
return jsonify({
97+
"needsAgreement": True,
98+
"agreementText": settings.get("user_agreement_text", ""),
99+
"enableDailyAcceptance": enable_daily_acceptance
100+
}), 200
101+
102+
@app.route("/api/user_agreement/accept", methods=["POST"])
103+
@swagger_route(security=get_auth_security())
104+
@login_required
105+
@user_required
106+
def api_accept_user_agreement():
107+
"""
108+
POST /api/user_agreement/accept
109+
Record that a user has accepted the user agreement for a workspace.
110+
111+
Body JSON:
112+
{
113+
workspace_id: str,
114+
workspace_type: str ('personal', 'group', 'public'),
115+
action_context: str (optional, e.g., 'file_upload', 'chat')
116+
}
117+
118+
Returns:
119+
{ success: bool, message: str }
120+
"""
121+
info = get_current_user_info()
122+
user_id = info["userId"]
123+
124+
data = request.get_json() or {}
125+
workspace_id = data.get("workspace_id")
126+
workspace_type = data.get("workspace_type")
127+
action_context = data.get("action_context", "file_upload")
128+
129+
if not workspace_id or not workspace_type:
130+
return jsonify({"error": "workspace_id and workspace_type are required"}), 400
131+
132+
# Validate workspace type
133+
valid_types = ["personal", "group", "public"]
134+
if workspace_type not in valid_types:
135+
return jsonify({"error": f"Invalid workspace_type. Must be one of: {', '.join(valid_types)}"}), 400
136+
137+
# Get workspace name for logging
138+
workspace_name = None
139+
if workspace_type == "public":
140+
ws = find_public_workspace_by_id(workspace_id)
141+
if ws:
142+
workspace_name = ws.get("name", "")
143+
144+
# Log the acceptance
145+
try:
146+
log_user_agreement_accepted(
147+
user_id=user_id,
148+
workspace_type=workspace_type,
149+
workspace_id=workspace_id,
150+
workspace_name=workspace_name,
151+
action_context=action_context
152+
)
153+
154+
debug_print(f"[USER_AGREEMENT] Recorded acceptance: user {user_id}, {workspace_type} workspace {workspace_id}")
155+
156+
return jsonify({
157+
"success": True,
158+
"message": "User agreement acceptance recorded"
159+
}), 200
160+
161+
except Exception as e:
162+
debug_print(f"[USER_AGREEMENT] Error recording acceptance: {str(e)}")
163+
log_event(f"Error recording user agreement acceptance: {str(e)}", level=logging.ERROR)
164+
return jsonify({
165+
"success": False,
166+
"error": f"Failed to record acceptance: {str(e)}"
167+
}), 500

0 commit comments

Comments
 (0)