Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 203d5e0575 |
@@ -134,3 +134,7 @@ To access from other devices on your network:
|
||||
1. Find your server's IP address
|
||||
2. Access via `http://YOUR_IP:5172`
|
||||
3. Make sure your firewall allows connections on port 5172
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -17,17 +17,11 @@ COPY static/ static/
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh .
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5172
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV FLASK_DEBUG=False
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Run the application via entrypoint
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
# Run the application
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
@@ -295,3 +295,7 @@ For manual Python installation:
|
||||
- Verify your Mailcow server is accessible from the machine running this app
|
||||
- Check that the API key is valid and has the correct permissions
|
||||
- Review logs in `data/malias2.log`
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
136
app.py
136
app.py
@@ -2,8 +2,6 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, s
|
||||
from functools import wraps
|
||||
import malias_wrapper as malias_w
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24) # Secret key for session management
|
||||
@@ -125,60 +123,6 @@ def delete_alias_route():
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(e)}"})
|
||||
|
||||
@app.route('/delete_aliases_bulk', methods=['POST'])
|
||||
@login_required
|
||||
def delete_aliases_bulk():
|
||||
"""Delete multiple aliases at once"""
|
||||
try:
|
||||
data = request.json
|
||||
aliases = data.get('aliases', [])
|
||||
|
||||
if not aliases or not isinstance(aliases, list):
|
||||
return jsonify({'status': 'error', 'message': 'Aliases array is required'})
|
||||
|
||||
if len(aliases) == 0:
|
||||
return jsonify({'status': 'error', 'message': 'No aliases provided'})
|
||||
|
||||
# Delete multiple aliases
|
||||
result = malias_w.delete_multiple_aliases(aliases)
|
||||
|
||||
# Build response message
|
||||
success_count = result['success_count']
|
||||
failed_count = result['failed_count']
|
||||
total_count = success_count + failed_count
|
||||
|
||||
if failed_count == 0:
|
||||
message = f"{success_count} of {total_count} aliases deleted successfully"
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': message,
|
||||
'details': result
|
||||
})
|
||||
elif success_count == 0:
|
||||
# All failed
|
||||
failed_list = ', '.join([f['alias'] for f in result['failed_aliases'][:3]])
|
||||
if len(result['failed_aliases']) > 3:
|
||||
failed_list += f" and {len(result['failed_aliases']) - 3} more"
|
||||
message = f"Failed to delete aliases: {failed_list}"
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': message,
|
||||
'details': result
|
||||
})
|
||||
else:
|
||||
# Partial success
|
||||
failed_list = ', '.join([f['alias'] for f in result['failed_aliases'][:3]])
|
||||
if len(result['failed_aliases']) > 3:
|
||||
failed_list += f" and {len(result['failed_aliases']) - 3} more"
|
||||
message = f"{success_count} of {total_count} aliases deleted successfully. Failed: {failed_list}"
|
||||
return jsonify({
|
||||
'status': 'partial',
|
||||
'message': message,
|
||||
'details': result
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': f"Error deleting aliases: {str(e)}"})
|
||||
|
||||
@app.route('/create_timed_alias', methods=['POST'])
|
||||
@login_required
|
||||
def create_timed_alias():
|
||||
@@ -265,82 +209,4 @@ def change_password_route():
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parse command-line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Mailcow Alias Manager - Web interface for managing mail aliases',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
python3 app.py Run in normal mode on port 5172
|
||||
python3 app.py --debug Run in debug mode with auto-reload
|
||||
python3 app.py --port 8080 Run on custom port 8080
|
||||
python3 app.py --debug --port 8080 Run in debug mode on port 8080
|
||||
python3 app.py --host 127.0.0.1 Bind to localhost only
|
||||
|
||||
Environment Variables (Docker):
|
||||
FLASK_DEBUG Set to 'True' for debug mode (default: False)
|
||||
FLASK_PORT Port to run on (default: 5172)
|
||||
FLASK_HOST Host to bind to (default: 0.0.0.0)
|
||||
FLASK_ENV Set to 'production' or 'development'
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Enable debug mode with auto-reload and detailed error messages'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--port',
|
||||
type=int,
|
||||
default=None,
|
||||
help='Port to run on (default: 5172 or FLASK_PORT env var)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Host to bind to (default: 0.0.0.0 or FLASK_HOST env var)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Priority: Command-line args > Environment variables > Defaults
|
||||
# Debug mode
|
||||
if args.debug:
|
||||
debug_mode = True
|
||||
else:
|
||||
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() in ('true', '1', 'yes')
|
||||
|
||||
# Port
|
||||
if args.port is not None:
|
||||
port = args.port
|
||||
else:
|
||||
port = int(os.getenv('FLASK_PORT', '5172'))
|
||||
|
||||
# Host
|
||||
if args.host is not None:
|
||||
host = args.host
|
||||
else:
|
||||
host = os.getenv('FLASK_HOST', '0.0.0.0')
|
||||
|
||||
# Print startup info
|
||||
print('=' * 60)
|
||||
print(' Mailcow Alias Manager - Starting...')
|
||||
print('=' * 60)
|
||||
print(f' Mode: {"DEBUG (Development)" if debug_mode else "NORMAL (Production)"}')
|
||||
print(f' Host: {host}')
|
||||
print(f' Port: {port}')
|
||||
print(f' URL: http://{host}:{port}')
|
||||
print('=' * 60)
|
||||
if debug_mode:
|
||||
print(' ⚠️ WARNING: Debug mode is enabled!')
|
||||
print(' - Auto-reload on code changes')
|
||||
print(' - Detailed error messages shown')
|
||||
print(' - NOT suitable for production use')
|
||||
print('=' * 60)
|
||||
print()
|
||||
|
||||
app.run(debug=debug_mode, host=host, port=port)
|
||||
app.run(debug=True, host='0.0.0.0', port=5172)
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
# Mailcow Alias Manager - Docker Compose Configuration
|
||||
#
|
||||
# Production vs Development Mode:
|
||||
# --------------------------------
|
||||
# FLASK_ENV=production + FLASK_DEBUG=False → Uses Gunicorn (4 workers, production-ready)
|
||||
# FLASK_ENV=development or FLASK_DEBUG=True → Uses Flask dev server (auto-reload, debug info)
|
||||
#
|
||||
# Recommended Settings:
|
||||
# - Production: FLASK_ENV=production, FLASK_DEBUG=False (default)
|
||||
# - Development: FLASK_ENV=development, FLASK_DEBUG=True
|
||||
|
||||
services:
|
||||
mailcow-alias-manager:
|
||||
image: gitlab.pm/rune/malias-web:latest
|
||||
@@ -19,17 +8,4 @@ services:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Oslo
|
||||
|
||||
# Flask Configuration
|
||||
# Set to 'production' for Gunicorn (recommended) or 'development' for Flask dev server
|
||||
- FLASK_ENV=production
|
||||
|
||||
# Debug mode: False for production (recommended), True for development/troubleshooting
|
||||
- FLASK_DEBUG=False
|
||||
|
||||
# Port configuration (default: 5172)
|
||||
- FLASK_PORT=5172
|
||||
|
||||
# Host binding (default: 0.0.0.0 for Docker)
|
||||
- FLASK_HOST=0.0.0.0
|
||||
- TZ=Europe/Oslo
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get environment variables with defaults
|
||||
FLASK_DEBUG="${FLASK_DEBUG:-False}"
|
||||
FLASK_ENV="${FLASK_ENV:-production}"
|
||||
FLASK_PORT="${FLASK_PORT:-5172}"
|
||||
FLASK_HOST="${FLASK_HOST:-0.0.0.0}"
|
||||
|
||||
echo "Starting Mailcow Alias Manager..."
|
||||
echo "Environment: $FLASK_ENV"
|
||||
echo "Debug mode: $FLASK_DEBUG"
|
||||
echo "Port: $FLASK_PORT"
|
||||
|
||||
# Check if we should run in production mode
|
||||
if [ "$FLASK_ENV" = "production" ] && [ "$FLASK_DEBUG" != "True" ] && [ "$FLASK_DEBUG" != "true" ] && [ "$FLASK_DEBUG" != "1" ]; then
|
||||
echo "Starting with Gunicorn (production mode)..."
|
||||
exec gunicorn --bind $FLASK_HOST:$FLASK_PORT \
|
||||
--workers 4 \
|
||||
--threads 2 \
|
||||
--timeout 120 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
--log-level info \
|
||||
"app:app"
|
||||
else
|
||||
echo "Starting with Flask development server (debug mode)..."
|
||||
exec python app.py
|
||||
fi
|
||||
@@ -107,12 +107,12 @@ def get_config():
|
||||
return {'mailcow_server': '', 'mailcow_api_key': ''}
|
||||
|
||||
def search_aliases(query):
|
||||
"""Search for aliases in local database (matches from start of alias address only)"""
|
||||
"""Search for aliases in local database"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
search_term = query + '%' # Match from start only
|
||||
cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ?',
|
||||
(search_term,))
|
||||
search_term = '%' + query + '%'
|
||||
cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ? OR goto LIKE ?',
|
||||
(search_term, search_term))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
return results
|
||||
@@ -384,37 +384,3 @@ def change_password(old_password, new_password):
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
def delete_multiple_aliases(alias_list):
|
||||
"""
|
||||
Delete multiple aliases in one operation
|
||||
|
||||
Args:
|
||||
alias_list: List of alias email addresses to delete
|
||||
|
||||
Returns:
|
||||
Dictionary with:
|
||||
- success_count: Number of successfully deleted aliases
|
||||
- failed_count: Number of failed deletions
|
||||
- failed_aliases: List of dicts with 'alias' and 'error' for each failure
|
||||
"""
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
failed_aliases = []
|
||||
|
||||
for alias in alias_list:
|
||||
try:
|
||||
delete_alias(alias)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
failed_aliases.append({
|
||||
'alias': alias,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
'success_count': success_count,
|
||||
'failed_count': failed_count,
|
||||
'failed_aliases': failed_aliases
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@ python-dotenv==1.0.0
|
||||
httpx==0.27.0
|
||||
rich==13.7.0
|
||||
bcrypt==4.1.2
|
||||
gunicorn==21.2.0
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 16px !important;
|
||||
color: #707170FF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #1a1a1a;
|
||||
@@ -273,6 +279,13 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #707170FF;
|
||||
align: center;
|
||||
padding-top: 20%;
|
||||
}
|
||||
|
||||
/* Mobile Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
@@ -441,93 +454,6 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Delete modal responsive styles */
|
||||
.delete-results-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.delete-results-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.delete-results-table tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.delete-results-table tr {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
background-color: #252525;
|
||||
border-radius: 5px;
|
||||
padding: 50px 15px 15px 15px;
|
||||
border: 1px solid #404040;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.delete-results-table td {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 6px 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Checkbox in top-left corner */
|
||||
.delete-results-table td:first-child {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.delete-checkbox {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Trash icon in top-right corner */
|
||||
.delete-results-table td:last-child {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Alias content with labels */
|
||||
.delete-results-table td:nth-child(2)::before {
|
||||
content: "Alias: ";
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.delete-results-table td:nth-child(3)::before {
|
||||
content: "Goes to: ";
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
/* Bulk controls stack vertically */
|
||||
.bulk-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bulk-controls .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Delete confirm modal */
|
||||
.delete-confirm-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* Results warning */
|
||||
.results-warning {
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet Styles */
|
||||
@@ -544,25 +470,6 @@
|
||||
margin-left: 0;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
/* Delete table on tablets - keep table format but adjust sizing */
|
||||
.delete-results-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.delete-results-table th,
|
||||
.delete-results-table td {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.delete-checkbox {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Mobile Devices */
|
||||
@@ -579,147 +486,6 @@
|
||||
.result-section h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Delete modal adjustments for very small screens */
|
||||
.config-content {
|
||||
padding: 15px 12px;
|
||||
}
|
||||
|
||||
.delete-results-table tr {
|
||||
padding: 45px 12px 12px 12px;
|
||||
}
|
||||
|
||||
.delete-results-table td:first-child {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.delete-results-table td:last-child {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.delete-confirm-list {
|
||||
max-height: 150px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete Aliases Modal Specific Styles */
|
||||
.delete-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: #0066cc;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
transition: transform 0.2s, filter 0.2s;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.delete-icon:hover {
|
||||
transform: scale(1.3);
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
|
||||
.delete-icon:active {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.delete-results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.delete-results-table th {
|
||||
background-color: #1a1a1a;
|
||||
color: #ffffff;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #404040;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.delete-results-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.delete-table-row {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.delete-table-row:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
.delete-table-row.selected {
|
||||
background-color: #1a3a5a !important;
|
||||
}
|
||||
|
||||
.delete-table-row.selected:hover {
|
||||
background-color: #244b6b !important;
|
||||
}
|
||||
|
||||
.results-warning {
|
||||
color: #ff9800;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.delete-confirm-list {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 5px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.delete-confirm-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.delete-confirm-list li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #404040;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.delete-confirm-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.delete-confirm-list li::before {
|
||||
content: "• ";
|
||||
color: #f44336;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Column widths for delete table */
|
||||
.delete-results-table th:first-child,
|
||||
.delete-results-table td:first-child {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-results-table th:last-child,
|
||||
.delete-results-table td:last-child {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -735,7 +501,7 @@
|
||||
<button class="btn" onclick="executeAction('sync_aliases')">Sync Aliases</button>
|
||||
<button class="btn" onclick="executeAction('get_domains')">Show Domains</button>
|
||||
<button class="btn" onclick="openCreateAlias()">Create Alias</button>
|
||||
<button class="btn" onclick="openDeleteAliasesModal()">Delete Aliases</button>
|
||||
<button class="btn" onclick="openDeleteAlias()">Delete Alias</button>
|
||||
<button class="btn" onclick="openTimedAlias()">Create Timed Alias</button>
|
||||
<button class="btn btn-config" onclick="openConfig()">Configuration</button>
|
||||
</div>
|
||||
@@ -743,7 +509,7 @@
|
||||
<div class="search-section">
|
||||
<h2 style="margin-bottom: 15px; color: #ffffff;">Search Aliases</h2>
|
||||
<div class="search-container">
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Start typing alias address (e.g., power...)">
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Search for alias or destination email...">
|
||||
<button class="btn btn-primary" onclick="performSearch()">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -812,53 +578,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Aliases Modal -->
|
||||
<!-- Delete Alias Modal -->
|
||||
<div class="config-modal" id="deleteAliasModal">
|
||||
<div class="config-content" style="max-width: 800px;">
|
||||
<h2>Delete Aliases</h2>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="form-group">
|
||||
<label for="deleteSearchInput">Search for aliases to delete</label>
|
||||
<div class="search-container">
|
||||
<input type="text" class="search-input" id="deleteSearchInput" placeholder="Start typing alias address (e.g., power...)">
|
||||
<button class="btn btn-primary" onclick="searchAliasesForDeletion()">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Section (initially hidden) -->
|
||||
<div id="deleteResultsSection" style="display: none; margin-top: 20px;">
|
||||
<div id="deleteResultsInfo" style="margin-bottom: 10px; color: #4CAF50;"></div>
|
||||
<div id="deleteResultsTable" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
|
||||
<!-- Bulk Selection Controls -->
|
||||
<div class="bulk-controls" style="display: flex; gap: 10px; margin: 15px 0; justify-content: center;">
|
||||
<button class="btn" onclick="selectAllDeleteAliases()">Select All</button>
|
||||
<button class="btn" onclick="deselectAllDeleteAliases()">Deselect All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="modal-buttons">
|
||||
<button class="btn" onclick="closeDeleteAliasesModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="deleteSelectedBtn" onclick="confirmDeleteSelected()" disabled>
|
||||
Delete Selected (0)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<div class="config-modal" id="deleteConfirmModal">
|
||||
<div class="config-content">
|
||||
<h2 style="color: #f44336;">⚠️ Confirm Deletion</h2>
|
||||
<div id="deleteConfirmContent"></div>
|
||||
<p style="color: #f44336; margin-top: 15px; font-weight: bold;">This action cannot be undone.</p>
|
||||
<h2>Delete Alias</h2>
|
||||
<div class="form-group">
|
||||
<label for="deleteAliasEmail">Alias Email to Delete</label>
|
||||
<input type="text" id="deleteAliasEmail" placeholder="alias@example.com">
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button class="btn" onclick="closeDeleteConfirm()">Cancel</button>
|
||||
<button class="btn" style="background-color: #d32f2f; border-color: #d32f2f;" onclick="executeDeleteSelected()">
|
||||
Yes, Delete All
|
||||
</button>
|
||||
<button class="btn" onclick="closeDeleteAlias()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="deleteAlias()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1146,258 +876,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Aliases Modal Functions
|
||||
let selectedAliases = new Set();
|
||||
let currentDeleteResults = [];
|
||||
|
||||
function openDeleteAliasesModal() {
|
||||
// Delete Alias Modal Functions
|
||||
function openDeleteAlias() {
|
||||
document.getElementById('deleteAliasModal').classList.add('active');
|
||||
document.getElementById('deleteSearchInput').value = '';
|
||||
document.getElementById('deleteResultsSection').style.display = 'none';
|
||||
selectedAliases.clear();
|
||||
currentDeleteResults = [];
|
||||
updateDeleteButtonText();
|
||||
}
|
||||
|
||||
function closeDeleteAliasesModal() {
|
||||
function closeDeleteAlias() {
|
||||
document.getElementById('deleteAliasModal').classList.remove('active');
|
||||
selectedAliases.clear();
|
||||
currentDeleteResults = [];
|
||||
}
|
||||
|
||||
async function searchAliasesForDeletion() {
|
||||
const query = document.getElementById('deleteSearchInput').value;
|
||||
|
||||
if (!query.trim()) {
|
||||
showResult('Please enter a search query', false);
|
||||
async function deleteAlias() {
|
||||
const alias = document.getElementById('deleteAliasEmail').value;
|
||||
|
||||
if (!alias) {
|
||||
showResult('Alias email is required', false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ query: query })
|
||||
});
|
||||
|
||||
if (checkAuth(response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
// Parse search results
|
||||
const results = [];
|
||||
const lines = data.message.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('→')) {
|
||||
const parts = line.split('→').map(p => p.trim());
|
||||
if (parts.length === 2) {
|
||||
results.push({
|
||||
alias: parts[0],
|
||||
goto: parts[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to 100 results
|
||||
const limited = results.slice(0, 100);
|
||||
const hasMore = results.length > 100;
|
||||
|
||||
currentDeleteResults = limited;
|
||||
renderDeleteResults(limited, hasMore, results.length);
|
||||
} else {
|
||||
document.getElementById('deleteResultsSection').style.display = 'none';
|
||||
showResult(data.message, false);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Search error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeleteResults(results, hasMore, totalCount) {
|
||||
if (results.length === 0) {
|
||||
document.getElementById('deleteResultsSection').style.display = 'none';
|
||||
showResult('No aliases found matching your search', false);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('deleteResultsSection').style.display = 'block';
|
||||
|
||||
// Info text
|
||||
let infoText = `Found ${results.length} alias(es)`;
|
||||
if (hasMore) {
|
||||
infoText += ` (showing first 100 of ${totalCount})`;
|
||||
}
|
||||
document.getElementById('deleteResultsInfo').innerHTML = infoText;
|
||||
|
||||
// Warning for too many results
|
||||
let warningHtml = '';
|
||||
if (hasMore) {
|
||||
warningHtml = `<div class="results-warning">⚠️ Showing first 100 of ${totalCount} results. Please refine your search for more specific results.</div>`;
|
||||
}
|
||||
|
||||
// Build table
|
||||
let html = warningHtml + `
|
||||
<table class="delete-results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Select</th>
|
||||
<th>Alias</th>
|
||||
<th>Goes To</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
const isSelected = selectedAliases.has(result.alias);
|
||||
html += `
|
||||
<tr class="delete-table-row ${isSelected ? 'selected' : ''}" id="delete-row-${i}">
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="delete-checkbox"
|
||||
id="checkbox-${i}"
|
||||
${isSelected ? 'checked' : ''}
|
||||
onchange="toggleAliasSelection('${escapeHtml(result.alias)}', ${i})">
|
||||
</td>
|
||||
<td>${escapeHtml(result.alias)}</td>
|
||||
<td>${escapeHtml(result.goto)}</td>
|
||||
<td>
|
||||
<span class="delete-icon" onclick="deleteSingleAliasQuick('${escapeHtml(result.alias)}', ${i})">🗑️</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
document.getElementById('deleteResultsTable').innerHTML = html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function toggleAliasSelection(alias, rowIndex) {
|
||||
const row = document.getElementById(`delete-row-${rowIndex}`);
|
||||
const checkbox = document.getElementById(`checkbox-${rowIndex}`);
|
||||
|
||||
if (selectedAliases.has(alias)) {
|
||||
selectedAliases.delete(alias);
|
||||
row.classList.remove('selected');
|
||||
} else {
|
||||
selectedAliases.add(alias);
|
||||
row.classList.add('selected');
|
||||
}
|
||||
|
||||
updateDeleteButtonText();
|
||||
}
|
||||
|
||||
function selectAllDeleteAliases() {
|
||||
selectedAliases.clear();
|
||||
for (const result of currentDeleteResults) {
|
||||
selectedAliases.add(result.alias);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
const checkboxes = document.querySelectorAll('.delete-checkbox');
|
||||
const rows = document.querySelectorAll('.delete-table-row');
|
||||
checkboxes.forEach(cb => cb.checked = true);
|
||||
rows.forEach(row => row.classList.add('selected'));
|
||||
|
||||
updateDeleteButtonText();
|
||||
}
|
||||
|
||||
function deselectAllDeleteAliases() {
|
||||
selectedAliases.clear();
|
||||
|
||||
// Update UI
|
||||
const checkboxes = document.querySelectorAll('.delete-checkbox');
|
||||
const rows = document.querySelectorAll('.delete-table-row');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
rows.forEach(row => row.classList.remove('selected'));
|
||||
|
||||
updateDeleteButtonText();
|
||||
}
|
||||
|
||||
function updateDeleteButtonText() {
|
||||
const btn = document.getElementById('deleteSelectedBtn');
|
||||
const count = selectedAliases.size;
|
||||
btn.textContent = `Delete Selected (${count})`;
|
||||
btn.disabled = count === 0;
|
||||
}
|
||||
|
||||
function confirmDeleteSelected() {
|
||||
if (selectedAliases.size === 0) return;
|
||||
|
||||
const aliasArray = Array.from(selectedAliases);
|
||||
let confirmHtml = `
|
||||
<p>You are about to delete <strong>${aliasArray.length}</strong> alias(es):</p>
|
||||
<div class="delete-confirm-list">
|
||||
<ul>
|
||||
`;
|
||||
|
||||
for (const alias of aliasArray) {
|
||||
confirmHtml += `<li>${escapeHtml(alias)}</li>`;
|
||||
}
|
||||
|
||||
confirmHtml += '</ul></div>';
|
||||
|
||||
document.getElementById('deleteConfirmContent').innerHTML = confirmHtml;
|
||||
document.getElementById('deleteConfirmModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeDeleteConfirm() {
|
||||
document.getElementById('deleteConfirmModal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function executeDeleteSelected() {
|
||||
const aliasArray = Array.from(selectedAliases);
|
||||
|
||||
if (aliasArray.length === 0) return;
|
||||
|
||||
closeDeleteConfirm();
|
||||
|
||||
try {
|
||||
const response = await fetch('/delete_aliases_bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ aliases: aliasArray })
|
||||
});
|
||||
|
||||
if (checkAuth(response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
showResult(data.message, data.status === 'success' || data.status === 'partial');
|
||||
|
||||
// Refresh search results
|
||||
if (data.status === 'success' || data.status === 'partial') {
|
||||
// Clear selection
|
||||
selectedAliases.clear();
|
||||
updateDeleteButtonText();
|
||||
|
||||
// Re-run search to show updated results
|
||||
await searchAliasesForDeletion();
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Error deleting aliases: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSingleAliasQuick(alias, rowIndex) {
|
||||
if (!confirm(`Delete ${alias}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/delete_alias', {
|
||||
method: 'POST',
|
||||
@@ -1407,41 +902,17 @@
|
||||
body: JSON.stringify({ alias: alias })
|
||||
});
|
||||
|
||||
if (checkAuth(response)) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
showResult(data.message, data.status === 'success');
|
||||
if (data.status === 'success') {
|
||||
// Remove from selected if it was selected
|
||||
selectedAliases.delete(alias);
|
||||
|
||||
// Remove from current results
|
||||
currentDeleteResults = currentDeleteResults.filter(r => r.alias !== alias);
|
||||
|
||||
// Re-render table
|
||||
renderDeleteResults(currentDeleteResults, false, currentDeleteResults.length);
|
||||
|
||||
showResult(`Alias ${alias} deleted successfully`, true);
|
||||
} else {
|
||||
showResult(data.message, false);
|
||||
document.getElementById('deleteAliasEmail').value = '';
|
||||
closeDeleteAlias();
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(`Error deleting alias: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow search on Enter key in delete modal
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deleteSearchInput = document.getElementById('deleteSearchInput');
|
||||
if (deleteSearchInput) {
|
||||
deleteSearchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchAliasesForDeletion();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Timed Alias Modal Functions
|
||||
function openTimedAlias() {
|
||||
document.getElementById('timedAliasModal').classList.add('active');
|
||||
@@ -1489,14 +960,15 @@
|
||||
if (e.target === this) closeCreateAlias();
|
||||
});
|
||||
document.getElementById('deleteAliasModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeDeleteAliasesModal();
|
||||
});
|
||||
document.getElementById('deleteConfirmModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeDeleteConfirm();
|
||||
if (e.target === this) closeDeleteAlias();
|
||||
});
|
||||
document.getElementById('timedAliasModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeTimedAlias();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
<div class="footer">
|
||||
<p style="padding-top:2%">© <a href="https://opensource.org/license/mit">MIT</a> 2026 Rune Olsen <a href="https://blog.rune.pm">Blog</a> — <a href="https://gitlab.pm/rune/malias-web">Source Code</a></p>
|
||||
</div>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user