Skip to content

Commit e0dc063

Browse files
Multi-domain feature added
1 parent 23006d6 commit e0dc063

File tree

9 files changed

+837
-192
lines changed

9 files changed

+837
-192
lines changed

app/directadmin_api.py

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,29 @@ def _make_request(self, endpoint, data=None, method='POST'):
6969

7070
# Format 1: URL encoded (key=value&key2=value2)
7171
if '=' in text and not text.startswith('<!'):
72-
# Handle special case for lists
72+
# Handle special case for lists with duplicate keys
7373
if 'list[]=' in text:
7474
items = []
7575
for part in text.split('&'):
7676
if part.startswith('list[]='):
7777
items.append(urllib.parse.unquote(part[7:]))
7878
return {'list': items}
7979

80-
# Standard key=value parsing
80+
# Standard key=value parsing - handle duplicate keys by collecting all values
8181
for pair in text.split('&'):
8282
if '=' in pair:
8383
key, value = pair.split('=', 1)
84-
result[urllib.parse.unquote(key)] = urllib.parse.unquote(value)
84+
key_decoded = urllib.parse.unquote(key)
85+
value_decoded = urllib.parse.unquote(value)
86+
87+
# If key already exists, convert to list or append to existing list
88+
if key_decoded in result:
89+
if not isinstance(result[key_decoded], list):
90+
# Convert existing single value to list
91+
result[key_decoded] = [result[key_decoded]]
92+
result[key_decoded].append(value_decoded)
93+
else:
94+
result[key_decoded] = value_decoded
8595

8696
# IMPORTANT: Check if this is an error response
8797
# error=0 means SUCCESS in DirectAdmin!
@@ -206,25 +216,45 @@ def get_email_accounts(self):
206216

207217
# Format 1: list format
208218
if 'list' in response:
209-
accounts = response['list']
210-
# Format 2: numbered keys (0, 1, 2, etc)
219+
list_data = response['list']
220+
if isinstance(list_data, list):
221+
accounts = list_data
222+
else:
223+
accounts = [list_data] # Single item, convert to list
224+
225+
# Format 2: list[] key (from fixed parsing)
226+
elif 'list[]' in response:
227+
list_data = response['list[]']
228+
if isinstance(list_data, list):
229+
accounts = list_data
230+
else:
231+
accounts = [list_data] # Single item, convert to list
232+
233+
# Format 3: numbered keys (0, 1, 2, etc)
211234
elif any(key.isdigit() for key in response.keys()):
212235
for key in sorted(response.keys()):
213236
if key.isdigit() and response[key]:
214237
if '@' in response[key]:
215238
accounts.append(response[key])
216239
else:
217240
accounts.append(f"{response[key]}@{self.domain}")
218-
# Format 3: email addresses as keys
241+
242+
# Format 4: email addresses as keys
219243
else:
220244
for key, value in response.items():
221245
if '@' in key and not key.startswith('error'):
222246
accounts.append(key)
223247
elif value and '@' in str(value):
224-
accounts.append(str(value))
248+
if isinstance(value, list):
249+
accounts.extend([str(v) for v in value])
250+
else:
251+
accounts.append(str(value))
225252
elif value and not key.startswith('error'):
226-
# Might be just username
227-
accounts.append(f"{value}@{self.domain}")
253+
# Might be just username(s)
254+
if isinstance(value, list):
255+
accounts.extend([f"{v}@{self.domain}" for v in value])
256+
else:
257+
accounts.append(f"{value}@{self.domain}")
228258

229259
elif isinstance(response, str) and response:
230260
print("Response is string, parsing...")
@@ -237,12 +267,24 @@ def get_email_accounts(self):
237267
elif line and not line.startswith('error'):
238268
accounts.append(f"{line}@{self.domain}")
239269

240-
# Remove duplicates and filter
241-
accounts = list(set(accounts))
270+
# Ensure all accounts have domain part
271+
processed_accounts = []
272+
for account in accounts:
273+
if account: # Skip empty strings
274+
if '@' not in account:
275+
# Add domain if missing
276+
processed_accounts.append(f"{account}@{self.domain}")
277+
else:
278+
processed_accounts.append(account)
279+
280+
# Remove duplicates and filter out API user
281+
processed_accounts = list(set(processed_accounts))
242282
api_email = f"{self.username}@{self.domain}"
243-
filtered = [email for email in accounts if email.lower() != api_email.lower()]
283+
filtered = [email for email in processed_accounts if email.lower() != api_email.lower()]
244284

245285
print(f"Found {len(filtered)} email accounts (excluding API user)")
286+
for account in filtered:
287+
print(f" - {account}")
246288
return sorted(filtered)
247289

248290
except Exception as e:

app/main.py

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,60 @@ def dashboard():
5555

5656
# ===== API Routes =====
5757

58+
@app.route('/api/domains', methods=['GET'])
59+
@login_required
60+
def get_user_domains():
61+
"""Get all domains for the current user"""
62+
try:
63+
domains = current_user.get_domains()
64+
return jsonify({
65+
'success': True,
66+
'domains': domains
67+
})
68+
except Exception as e:
69+
print(f"Error in /api/domains: {str(e)}")
70+
traceback.print_exc()
71+
return jsonify({
72+
'error': 'Failed to fetch domains',
73+
'domains': []
74+
}), 500
75+
5876
@app.route('/api/email-accounts', methods=['GET'])
5977
@login_required
6078
def get_email_accounts():
61-
"""Get all email accounts for the configured domain"""
79+
"""Get all email accounts for the specified domain"""
6280
if not current_user.has_da_config():
6381
return jsonify({
6482
'error': 'DirectAdmin not configured',
6583
'accounts': []
6684
}), 400
6785

6886
try:
87+
# Get domain from query parameter or use first domain
88+
domain = request.args.get('domain')
89+
if not domain:
90+
domain = current_user.get_first_domain()
91+
92+
if not domain:
93+
return jsonify({
94+
'error': 'No domain specified',
95+
'accounts': []
96+
}), 400
97+
98+
# Verify user has access to this domain
99+
user_domains = current_user.get_domains()
100+
if domain not in user_domains:
101+
return jsonify({
102+
'error': 'Access denied to domain',
103+
'accounts': []
104+
}), 403
105+
69106
# Create API instance
70107
api = DirectAdminAPI(
71108
current_user.da_server,
72109
current_user.da_username,
73110
current_user.get_da_password(),
74-
current_user.da_domain
111+
domain
75112
)
76113

77114
# Get email accounts
@@ -81,11 +118,12 @@ def get_email_accounts():
81118
if not isinstance(accounts, list):
82119
accounts = []
83120

84-
print(f"API returning {len(accounts)} email accounts")
121+
print(f"API returning {len(accounts)} email accounts for domain {domain}")
85122

86123
return jsonify({
87124
'success': True,
88-
'accounts': accounts
125+
'accounts': accounts,
126+
'domain': domain
89127
})
90128

91129
except Exception as e:
@@ -99,20 +137,39 @@ def get_email_accounts():
99137
@app.route('/api/forwarders', methods=['GET'])
100138
@login_required
101139
def get_forwarders():
102-
"""Get all email forwarders"""
140+
"""Get all email forwarders for the specified domain"""
103141
if not current_user.has_da_config():
104142
return jsonify({
105143
'error': 'DirectAdmin not configured',
106144
'forwarders': []
107145
}), 400
108146

109147
try:
148+
# Get domain from query parameter or use first domain
149+
domain = request.args.get('domain')
150+
if not domain:
151+
domain = current_user.get_first_domain()
152+
153+
if not domain:
154+
return jsonify({
155+
'error': 'No domain specified',
156+
'forwarders': []
157+
}), 400
158+
159+
# Verify user has access to this domain
160+
user_domains = current_user.get_domains()
161+
if domain not in user_domains:
162+
return jsonify({
163+
'error': 'Access denied to domain',
164+
'forwarders': []
165+
}), 403
166+
110167
# Create API instance
111168
api = DirectAdminAPI(
112169
current_user.da_server,
113170
current_user.da_username,
114171
current_user.get_da_password(),
115-
current_user.da_domain
172+
domain
116173
)
117174

118175
# Get forwarders
@@ -122,11 +179,12 @@ def get_forwarders():
122179
if not isinstance(forwarders, list):
123180
forwarders = []
124181

125-
print(f"API returning {len(forwarders)} forwarders")
182+
print(f"API returning {len(forwarders)} forwarders for domain {domain}")
126183

127184
return jsonify({
128185
'success': True,
129-
'forwarders': forwarders
186+
'forwarders': forwarders,
187+
'domain': domain
130188
})
131189

132190
except Exception as e:
@@ -152,6 +210,7 @@ def create_forwarder():
152210

153211
address = data.get('address', '').strip()
154212
destination = data.get('destination', '').strip()
213+
domain = data.get('domain', '').strip()
155214

156215
# Validate inputs
157216
if not address:
@@ -160,12 +219,24 @@ def create_forwarder():
160219
if not destination:
161220
return jsonify({'error': 'Destination email is required'}), 400
162221

222+
# Get domain or use first domain
223+
if not domain:
224+
domain = current_user.get_first_domain()
225+
226+
if not domain:
227+
return jsonify({'error': 'No domain specified'}), 400
228+
229+
# Verify user has access to this domain
230+
user_domains = current_user.get_domains()
231+
if domain not in user_domains:
232+
return jsonify({'error': 'Access denied to domain'}), 403
233+
163234
# Create API instance
164235
api = DirectAdminAPI(
165236
current_user.da_server,
166237
current_user.da_username,
167238
current_user.get_da_password(),
168-
current_user.da_domain
239+
domain
169240
)
170241

171242
# Create the forwarder
@@ -174,7 +245,8 @@ def create_forwarder():
174245
if success:
175246
return jsonify({
176247
'success': True,
177-
'message': message
248+
'message': message,
249+
'domain': domain
178250
})
179251
else:
180252
return jsonify({
@@ -202,16 +274,31 @@ def delete_forwarder():
202274
return jsonify({'error': 'No data provided'}), 400
203275

204276
address = data.get('address', '').strip()
277+
domain = data.get('domain', '').strip()
205278

206279
if not address:
207280
return jsonify({'error': 'Email address is required'}), 400
208281

282+
# Extract domain from address if not provided
283+
if not domain and '@' in address:
284+
domain = address.split('@')[1]
285+
elif not domain:
286+
domain = current_user.get_first_domain()
287+
288+
if not domain:
289+
return jsonify({'error': 'No domain specified'}), 400
290+
291+
# Verify user has access to this domain
292+
user_domains = current_user.get_domains()
293+
if domain not in user_domains:
294+
return jsonify({'error': 'Access denied to domain'}), 403
295+
209296
# Create API instance
210297
api = DirectAdminAPI(
211298
current_user.da_server,
212299
current_user.da_username,
213300
current_user.get_da_password(),
214-
current_user.da_domain
301+
domain
215302
)
216303

217304
# Delete the forwarder
@@ -220,7 +307,8 @@ def delete_forwarder():
220307
if success:
221308
return jsonify({
222309
'success': True,
223-
'message': message
310+
'message': message,
311+
'domain': domain
224312
})
225313
else:
226314
return jsonify({
@@ -271,34 +359,7 @@ def handle_exception(error):
271359

272360
# ===== Database Initialization =====
273361

274-
# Ensure DB initialization only once (important with multi-worker if --preload not used)
275-
if not app.config.get('_DB_INITIALIZED', False):
276-
with app.app_context():
277-
print(f"Initializing database at URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
278-
db.create_all()
279-
280-
# Create default admin user only if no administrators exist
281-
admin_count = User.query.filter_by(is_admin=True).count()
282-
if admin_count == 0:
283-
# No administrators exist, create default admin user
284-
admin_user = User(username='admin', is_admin=True)
285-
admin_user.set_password('changeme') # Default password
286-
db.session.add(admin_user)
287-
try:
288-
db.session.commit()
289-
print("=" * 50)
290-
print("Default admin user created!")
291-
print("Username: admin")
292-
print("Password: changeme")
293-
print("PLEASE CHANGE THIS PASSWORD IMMEDIATELY!")
294-
print("=" * 50)
295-
except Exception as e:
296-
print(f"Error creating admin user: {e}")
297-
db.session.rollback()
298-
else:
299-
print(f"Found {admin_count} administrator(s) - skipping default admin creation")
300-
301-
app.config['_DB_INITIALIZED'] = True
362+
# Ensure DB initialization only once (important with multi-worker if --preload not used)\n if not app.config.get('_DB_INITIALIZED', False):\n with app.app_context():\n print(f\"Initializing database at URI: {app.config['SQLALCHEMY_DATABASE_URI']}\")\n db.create_all()\n\n # Migrate existing users from single domain to multi-domain\n print(\"Checking for users to migrate to multi-domain...\")\n users_to_migrate = User.query.filter(\n User.da_domain.isnot(None),\n ~User.domains.any()\n ).all()\n \n for user in users_to_migrate:\n print(f\"Migrating user {user.username} with domain {user.da_domain}\")\n success, message = user.add_domain(user.da_domain)\n if success:\n print(f\" ✓ Migrated {user.username}: {message}\")\n else:\n print(f\" ✗ Failed to migrate {user.username}: {message}\")\n \n if users_to_migrate:\n try:\n db.session.commit()\n print(f\"Successfully migrated {len(users_to_migrate)} users to multi-domain.\")\n except Exception as e:\n print(f\"Error during migration: {e}\")\n db.session.rollback()\n\n # Create default admin user only if no administrators exist\n admin_count = User.query.filter_by(is_admin=True).count()\n if admin_count == 0:\n # No administrators exist, create default admin user\n admin_user = User(username='admin', is_admin=True)\n admin_user.set_password('changeme') # Default password\n db.session.add(admin_user)\n try:\n db.session.commit()\n print(\"=\" * 50)\n print(\"Default admin user created!\")\n print(\"Username: admin\")\n print(\"Password: changeme\")\n print(\"PLEASE CHANGE THIS PASSWORD IMMEDIATELY!\")\n print(\"=\" * 50)\n except Exception as e:\n print(f\"Error creating admin user: {e}\")\n db.session.rollback()\n else:\n print(f\"Found {admin_count} administrator(s) - skipping default admin creation\")\n\n app.config['_DB_INITIALIZED'] = True
302363

303364
# ===== Additional App Configuration =====
304365

0 commit comments

Comments
 (0)