@@ -60,6 +60,7 @@ def flock(fd, op):
6060from simple_org_chart .config import EMPLOYEE_LIST_FILE
6161from simple_org_chart .auth import login_required , require_auth , sanitize_next_path
6262from simple_org_chart .email_config import (
63+ DEFAULT_EMAIL_CONFIG ,
6364 get_smtp_config ,
6465 is_smtp_configured ,
6566 load_email_config ,
@@ -1128,6 +1129,7 @@ def reset_all_settings():
11281129 os .remove (logo_path )
11291130
11301131 save_settings (DEFAULT_SETTINGS )
1132+ save_email_config (DEFAULT_EMAIL_CONFIG )
11311133
11321134 threading .Thread (target = restart_scheduler ).start ()
11331135
@@ -1137,6 +1139,89 @@ def reset_all_settings():
11371139 return jsonify ({'error' : 'Reset failed' }), 500
11381140
11391141
1142+ # ---------------------------------------------------------------------------
1143+ # Config export / import
1144+ # ---------------------------------------------------------------------------
1145+
1146+ @app .route ('/api/settings/export' , methods = ['GET' ])
1147+ @require_auth
1148+ def export_settings ():
1149+ """Download all configuration as a single flat JSON file."""
1150+ settings = load_settings ()
1151+ email_config = load_email_config ()
1152+ # Merge everything flat; strip transient runtime fields and non-default keys
1153+ settings_public = {k : v for k , v in settings .items () if k in DEFAULT_SETTINGS }
1154+ merged = {}
1155+ merged .update (settings_public )
1156+ merged .update ({k : v for k , v in email_config .items () if k != 'lastSent' })
1157+ payload = json .dumps (merged , indent = 2 )
1158+ return app .response_class (
1159+ payload ,
1160+ mimetype = 'application/json' ,
1161+ headers = {
1162+ 'Content-Disposition' : 'attachment; filename="simple-org-chart-config.json"' ,
1163+ },
1164+ )
1165+
1166+
1167+ @app .route ('/api/settings/import' , methods = ['POST' ])
1168+ @require_auth
1169+ def import_settings ():
1170+ """Import configuration from an uploaded JSON file."""
1171+ uploaded = request .files .get ('file' )
1172+ if not uploaded :
1173+ return jsonify ({'error' : 'No file uploaded' }), 400
1174+
1175+ # Enforce file size limit consistent with other upload endpoints
1176+ uploaded .seek (0 , 2 )
1177+ file_size = uploaded .tell ()
1178+ uploaded .seek (0 )
1179+ if file_size > MAX_FILE_SIZE :
1180+ return jsonify ({'error' : f'File too large. Maximum size: { MAX_FILE_SIZE // (1024 * 1024 )} MB' }), 400
1181+
1182+ try :
1183+ raw = uploaded .read ()
1184+ incoming = json .loads (raw )
1185+ except (json .JSONDecodeError , UnicodeDecodeError ) as exc :
1186+ logger .warning ("Config import: invalid JSON – %s" , exc )
1187+ return jsonify ({'error' : 'Invalid JSON file' }), 400
1188+
1189+ if not isinstance (incoming , dict ):
1190+ return jsonify ({'error' : 'Expected a JSON object' }), 400
1191+
1192+ # Split incoming keys into settings vs email config
1193+ email_keys = set (DEFAULT_EMAIL_CONFIG .keys ()) - {'lastSent' }
1194+ settings_data = {}
1195+ email_data = {}
1196+
1197+ for key , value in incoming .items ():
1198+ if key in DEFAULT_SETTINGS :
1199+ settings_data [key ] = value
1200+ elif key in email_keys :
1201+ email_data [key ] = value
1202+
1203+ # Only save settings if at least one valid settings key was provided
1204+ settings_ok = True
1205+ if settings_data :
1206+ settings_ok = save_settings (settings_data )
1207+
1208+ email_ok = True
1209+ if email_data :
1210+ email_ok = save_email_config (email_data )
1211+
1212+ if settings_ok and email_ok :
1213+ if 'updateTime' in settings_data or 'autoUpdateEnabled' in settings_data :
1214+ threading .Thread (target = restart_scheduler , daemon = True ).start ()
1215+ return jsonify ({'success' : True , 'settings' : load_settings ()})
1216+
1217+ errors = []
1218+ if not settings_ok :
1219+ errors .append ('settings' )
1220+ if not email_ok :
1221+ errors .append ('email config' )
1222+ return jsonify ({'error' : f'Failed to save: { ", " .join (errors )} ' }), 500
1223+
1224+
11401225@app .route ('/api/email-config' , methods = ['GET' , 'POST' ])
11411226@require_auth
11421227@limiter .limit (RATE_LIMIT_SETTINGS )
0 commit comments