Skip to content

Commit 0d86851

Browse files
authored
Merge pull request #12 from ethereum/monet-requests
Monet requests
2 parents 964d32a + 975474f commit 0d86851

File tree

14 files changed

+1180
-122
lines changed

14 files changed

+1180
-122
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ AWS_REGION='us-east-1'
55
SES_FROM_EMAIL=''
66
TURNSTILE_SITE_KEY=''
77
TURNSTILE_SECRET_KEY=''
8+
KISSFLOW_SUBDOMAIN='ethereum'
9+
KISSFLOW_ACCESS_KEY_ID=''
10+
KISSFLOW_ACCESS_KEY_SECRET=''
11+
KISSFLOW_ACCOUNT_ID=''
12+
KISSFLOW_PROCESS_ID=''

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
prime-env.sh
12
.DS_Store
23
.env
34
.flaskenv
@@ -23,4 +24,4 @@ secure-drop.log
2324
htmlcov/
2425
.coverage
2526
.coverage.*
26-
*,cover
27+
*,cover

.infisical.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"workspaceId": "6b03f5f8-6c53-4cd6-8fa9-6a56f6804adb",
3+
"defaultEnvironment": "",
4+
"gitBranchToEnvironmentMapping": null
5+
}

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,30 @@ Docker Compose.
1818

1919
### Third Party Services
2020

21-
* Sendgrid
22-
* Google reCAPTCHA
21+
* AWS SES (for email delivery)
22+
* Cloudflare Turnstile (for bot protection)
23+
* Kissflow API (optional - for KYC submission tracking)
2324

2425

2526
## New setup
2627

2728
Make a fork of the repository. Set environment variables in `.env` file, using the provided example. Customise the templates and code. Update public keys in [static/js/public-keys.js](static/js/public-keys.js). Deploy to your web server or K8s cluster.
2829

30+
### Kissflow Integration (Optional)
31+
32+
The application now supports automatic integration with Kissflow for KYC submission tracking. When enabled, legal submissions with a Grant ID will automatically update the corresponding AOG (Approval of Grants) item in Kissflow with the submission identifier.
33+
34+
To enable Kissflow integration:
35+
1. Add the following to your `.env` file:
36+
```
37+
KISSFLOW_SUBDOMAIN=ethereum
38+
KISSFLOW_ACCESS_KEY_ID=your_access_key_id
39+
KISSFLOW_ACCESS_KEY_SECRET=your_access_key_secret
40+
KISSFLOW_ACCOUNT_ID=your_account_id
41+
KISSFLOW_PROCESS_ID=your_aog_process_id
42+
```
43+
2. Ensure your Kissflow API has permissions to read and update AOG items
44+
3. Test the integration using `python test_kissflow_integration.py`
2945

3046
## Security
3147

@@ -39,4 +55,4 @@ A server operator should follow best practises for security when setting up and
3955
docker compose up
4056
```
4157

42-
The server will be listening on 4200 port.
58+
The server will be listening on 4200 port.

docker-compose.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.8'
2-
31
services:
42
web:
53
build: .
@@ -12,7 +10,3 @@ services:
1210
FLASK_APP: server.py
1311
FLASK_DEBUG: ${DEBUG}
1412
DEBUG: ${DEBUG}
15-
SENDGRIDFROMEMAIL: ${SENDGRIDFROMEMAIL}
16-
SENDGRIDAPIKEY: ${SENDGRIDAPIKEY}
17-
RECAPTCHASITEKEY: ${RECAPTCHASITEKEY}
18-
RECAPTCHASECRETKEY: ${RECAPTCHASECRETKEY}

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "secure-drop"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"boto3==1.26.137",
9+
"flask==2.2.2",
10+
"flask-limiter==3.8.0",
11+
"flask-recaptcha==0.4.2",
12+
"gunicorn==20.1.0",
13+
"jinja2==3.0.3",
14+
"python-dotenv==0.21.0",
15+
"werkzeug==2.2.2",
16+
]

requirements.txt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
Flask==2.2.2
2-
Flask-ReCaptcha==0.4.2
3-
Jinja2==3.0.3
4-
python-dotenv==0.21.0
5-
boto3==1.26.137
6-
gunicorn==20.1.0
7-
Werkzeug==2.2.2
8-
Flask-Limiter==3.8.0
1+
Flask==3.1.1
2+
Jinja2==3.1.6
3+
python-dotenv==1.1.1
4+
boto3==1.39.8
5+
gunicorn==23.0.0
6+
Werkzeug==3.1.3
7+
Flask-Limiter==3.11.0
8+
requests==2.32.4

server.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from random import Random
55
import requests
66
import base64
7+
import json
78

89
from flask import Flask, render_template, request, jsonify
910
from flask_limiter import Limiter
@@ -184,6 +185,176 @@ def get_forwarded_address():
184185
# Otherwise use the default function
185186
return get_remote_address()
186187

188+
def find_aog_item_by_grant_id(grant_id):
189+
"""
190+
Finds an AOG (Approval of Grants) item in Kissflow by Grant ID.
191+
Uses the admin endpoint to get all items and searches through them.
192+
Returns the item ID if found, None otherwise.
193+
"""
194+
try:
195+
subdomain = os.getenv('KISSFLOW_SUBDOMAIN', 'ethereum')
196+
access_key_id = os.getenv('KISSFLOW_ACCESS_KEY_ID')
197+
access_key_secret = os.getenv('KISSFLOW_ACCESS_KEY_SECRET')
198+
account_id = os.getenv('KISSFLOW_ACCOUNT_ID')
199+
process_id = os.getenv('KISSFLOW_PROCESS_ID')
200+
201+
if not all([access_key_id, access_key_secret, account_id, process_id]):
202+
logging.error("Missing Kissflow configuration")
203+
return None
204+
205+
headers = {
206+
'Accept': 'application/json',
207+
'X-Access-Key-Id': access_key_id,
208+
'X-Access-Key-Secret': access_key_secret
209+
}
210+
211+
# Use admin endpoint to get all items
212+
page_number = 1
213+
page_size = 100 # Get 100 items per page
214+
215+
while True:
216+
# Kissflow admin API endpoint to get all items
217+
url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/item"
218+
219+
params = {
220+
'page_number': page_number,
221+
'page_size': page_size,
222+
'apply_preference': False
223+
}
224+
225+
response = requests.get(url, headers=headers, params=params)
226+
227+
if response.status_code != 200:
228+
logging.error(f"Kissflow API error: {response.status_code} - {response.text}")
229+
return None
230+
231+
data = response.json()
232+
233+
# The response structure contains table data with items
234+
# Look for items in the response structure
235+
items_found = []
236+
237+
# Check if there's a table structure in the response
238+
for key, val in data.items():
239+
if key != "Data":
240+
continue
241+
242+
if isinstance(val, list):
243+
for page_data in val:
244+
if isinstance(page_data, dict) and '_created_by' in page_data:
245+
items_found.append(page_data)
246+
247+
#print(items_found)
248+
# Search through the items for matching Grant ID
249+
for item in items_found:
250+
# Check various possible field names for the Grant ID
251+
grant_id_fields = ['Request_number', 'GrantId', 'Grant_ID', 'grant_id', 'PONumber']
252+
253+
for field in grant_id_fields:
254+
if field in item and str(item[field]) == str(grant_id):
255+
logging.info(f"Found AOG item with ID {item.get('_id')} for Grant ID {grant_id}")
256+
return item.get('_id')
257+
258+
# If we found fewer items than page_size, we've reached the end
259+
if len(items_found) < page_size:
260+
break
261+
262+
page_number += 1
263+
264+
# Safety check to prevent infinite loops
265+
if page_number > 100: # Max 10,000 items (100 pages * 100 items)
266+
logging.warning("Reached maximum page limit while searching for Grant ID")
267+
break
268+
269+
logging.warning(f"No AOG item found for Grant ID: {grant_id}")
270+
return None
271+
272+
except Exception as e:
273+
logging.error(f"Error finding AOG item: {str(e)}")
274+
275+
return None
276+
277+
def update_aog_kyc_comments(item_id, legal_identifier):
278+
"""
279+
Updates the KYC_Comments field in a Kissflow AOG item with the legal identifier.
280+
Uses the admin PUT endpoint to update item details.
281+
"""
282+
try:
283+
subdomain = os.getenv('KISSFLOW_SUBDOMAIN', 'ethereum')
284+
access_key_id = os.getenv('KISSFLOW_ACCESS_KEY_ID')
285+
access_key_secret = os.getenv('KISSFLOW_ACCESS_KEY_SECRET')
286+
account_id = os.getenv('KISSFLOW_ACCOUNT_ID')
287+
process_id = os.getenv('KISSFLOW_PROCESS_ID')
288+
289+
if not all([access_key_id, access_key_secret, account_id, process_id]):
290+
logging.error("Missing Kissflow configuration")
291+
return False
292+
293+
# First, get the current item details to preserve existing data
294+
headers = {
295+
'Accept': 'application/json',
296+
'Content-Type': 'application/json',
297+
'X-Access-Key-Id': access_key_id,
298+
'X-Access-Key-Secret': access_key_secret
299+
}
300+
301+
# Get current item details using admin endpoint
302+
get_url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/{item_id}"
303+
get_response = requests.get(get_url, headers=headers)
304+
305+
if get_response.status_code != 200:
306+
logging.error(f"Failed to get current item details: {get_response.status_code} - {get_response.text}")
307+
return False
308+
309+
current_item = get_response.json()
310+
311+
# Update the KYC_Comments field while preserving other fields
312+
current_kyc = current_item['KYC_Comments']
313+
314+
if current_kyc != "":
315+
current_item['KYC_Comments'] = current_kyc + "\n" + legal_identifier
316+
else:
317+
current_item['KYC_Comments'] = legal_identifier
318+
319+
# Remove all fields starting with '_' before sending to Kissflow
320+
filtered_item = {k: v for k, v in current_item.items() if not k.startswith('_')}
321+
322+
# Use admin PUT endpoint to update the item
323+
put_url = f"https://{subdomain}.kissflow.com/process/2/{account_id}/admin/{process_id}/{item_id}"
324+
325+
response = requests.put(put_url, headers=headers, json=filtered_item)
326+
327+
if response.status_code == 200:
328+
logging.info(f"Successfully updated AOG item {item_id} with legal identifier {legal_identifier}")
329+
return True
330+
else:
331+
logging.error(f"Kissflow API error: {response.status_code} - {response.text}")
332+
333+
except Exception as e:
334+
logging.error(f"Error updating AOG item: {str(e)}")
335+
336+
return False
337+
338+
def send_identifier_to_kissflow(grant_id, legal_identifier):
339+
"""
340+
Sends the legal identifier to the Kissflow AOG item based on Grant ID.
341+
"""
342+
if not grant_id:
343+
logging.warning("No Grant ID provided, skipping Kissflow update")
344+
return False
345+
346+
# Find the AOG item by Grant ID
347+
item_id = find_aog_item_by_grant_id(grant_id)
348+
349+
if not item_id:
350+
logging.warning(f"No AOG item found for Grant ID: {grant_id}")
351+
return False
352+
353+
# Update the KYC_Comments field
354+
success = update_aog_kyc_comments(item_id, legal_identifier)
355+
356+
return success
357+
187358
# Validate required environment variables
188359
required_env_vars = ['TURNSTILE_SITE_KEY', 'TURNSTILE_SECRET_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'SES_FROM_EMAIL']
189360
validate_env_vars(required_env_vars)
@@ -264,6 +435,16 @@ def submit():
264435

265436
send_email(message)
266437

438+
# If this is a legal submission with a Grant ID (reference), send to Kissflow
439+
if recipient == 'legal' and reference:
440+
kissflow_success = send_identifier_to_kissflow(reference, identifier)
441+
if kissflow_success:
442+
logging.info(f"Successfully sent identifier {identifier} to Kissflow for Grant ID {reference}")
443+
else:
444+
logging.warning(f"Failed to send identifier {identifier} to Kissflow for Grant ID {reference}")
445+
# Note: We don't fail the submission if Kissflow update fails
446+
# The email has already been sent successfully
447+
267448
notice = f'Thank you! The relevant team was notified of your submission. Please record the identifier and refer to it in correspondence: {identifier}'
268449

269450
return jsonify({'status': 'success', 'message': notice})

0 commit comments

Comments
 (0)