1 Commits
1.0.3 ... 1.0.4

Author SHA1 Message Date
d444bb5fcb Updated and fixes. v1.0.4 2026-01-16 11:13:32 +01:00
9 changed files with 802 additions and 54 deletions

View File

@@ -134,7 +134,3 @@ To access from other devices on your network:
1. Find your server's IP address 1. Find your server's IP address
2. Access via `http://YOUR_IP:5172` 2. Access via `http://YOUR_IP:5172`
3. Make sure your firewall allows connections on port 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.

View File

@@ -17,11 +17,17 @@ COPY static/ static/
# Create data directory # Create data directory
RUN mkdir -p /app/data RUN mkdir -p /app/data
# Copy entrypoint script
COPY docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh
# Expose port # Expose port
EXPOSE 5172 EXPOSE 5172
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV FLASK_DEBUG=False
ENV FLASK_ENV=production
# Run the application # Run the application via entrypoint
CMD ["python", "app.py"] ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@@ -295,7 +295,3 @@ For manual Python installation:
- Verify your Mailcow server is accessible from the machine running this app - Verify your Mailcow server is accessible from the machine running this app
- Check that the API key is valid and has the correct permissions - Check that the API key is valid and has the correct permissions
- Review logs in `data/malias2.log` - 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
View File

@@ -2,6 +2,8 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, s
from functools import wraps from functools import wraps
import malias_wrapper as malias_w import malias_wrapper as malias_w
import os import os
import argparse
import sys
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.urandom(24) # Secret key for session management app.secret_key = os.urandom(24) # Secret key for session management
@@ -123,6 +125,60 @@ def delete_alias_route():
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': f"Error deleting alias: {str(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']) @app.route('/create_timed_alias', methods=['POST'])
@login_required @login_required
def create_timed_alias(): def create_timed_alias():
@@ -209,4 +265,82 @@ def change_password_route():
return jsonify({'status': 'error', 'message': str(e)}) return jsonify({'status': 'error', 'message': str(e)})
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5172) # 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)

View File

@@ -1,3 +1,14 @@
# 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: services:
mailcow-alias-manager: mailcow-alias-manager:
image: gitlab.pm/rune/malias-web:latest image: gitlab.pm/rune/malias-web:latest
@@ -8,4 +19,17 @@ services:
- ./data:/app/data - ./data:/app/data
restart: unless-stopped restart: unless-stopped
environment: environment:
- TZ=Europe/Oslo - 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

29
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/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

View File

@@ -107,12 +107,12 @@ def get_config():
return {'mailcow_server': '', 'mailcow_api_key': ''} return {'mailcow_server': '', 'mailcow_api_key': ''}
def search_aliases(query): def search_aliases(query):
"""Search for aliases in local database""" """Search for aliases in local database (matches from start of alias address only)"""
conn = get_db_connection() conn = get_db_connection()
cursor = conn.cursor() cursor = conn.cursor()
search_term = '%' + query + '%' search_term = query + '%' # Match from start only
cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ? OR goto LIKE ?', cursor.execute('SELECT alias, goto FROM aliases WHERE alias LIKE ?',
(search_term, search_term)) (search_term,))
results = cursor.fetchall() results = cursor.fetchall()
conn.close() conn.close()
return results return results
@@ -384,3 +384,37 @@ def change_password(old_password, new_password):
conn.close() conn.close()
return True 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
}

View File

@@ -3,3 +3,4 @@ python-dotenv==1.0.0
httpx==0.27.0 httpx==0.27.0
rich==13.7.0 rich==13.7.0
bcrypt==4.1.2 bcrypt==4.1.2
gunicorn==21.2.0

View File

@@ -16,12 +16,6 @@
box-sizing: border-box; box-sizing: border-box;
} }
.footer {
font-size: 16px !important;
color: #707170FF;
text-align: center;
}
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #1a1a1a; background-color: #1a1a1a;
@@ -279,13 +273,6 @@
justify-content: flex-end; justify-content: flex-end;
} }
.footer {
font-size: 12px;
color: #707170FF;
align: center;
padding-top: 20%;
}
/* Mobile Responsive Styles */ /* Mobile Responsive Styles */
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {
@@ -454,6 +441,93 @@
font-size: 16px; 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 */ /* Tablet Styles */
@@ -470,6 +544,25 @@
margin-left: 0; margin-left: 0;
flex: 1 1 100%; 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 */ /* Small Mobile Devices */
@@ -486,6 +579,147 @@
.result-section h2 { .result-section h2 {
font-size: 16px; 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> </style>
</head> </head>
@@ -501,7 +735,7 @@
<button class="btn" onclick="executeAction('sync_aliases')">Sync Aliases</button> <button class="btn" onclick="executeAction('sync_aliases')">Sync Aliases</button>
<button class="btn" onclick="executeAction('get_domains')">Show Domains</button> <button class="btn" onclick="executeAction('get_domains')">Show Domains</button>
<button class="btn" onclick="openCreateAlias()">Create Alias</button> <button class="btn" onclick="openCreateAlias()">Create Alias</button>
<button class="btn" onclick="openDeleteAlias()">Delete Alias</button> <button class="btn" onclick="openDeleteAliasesModal()">Delete Aliases</button>
<button class="btn" onclick="openTimedAlias()">Create Timed Alias</button> <button class="btn" onclick="openTimedAlias()">Create Timed Alias</button>
<button class="btn btn-config" onclick="openConfig()">Configuration</button> <button class="btn btn-config" onclick="openConfig()">Configuration</button>
</div> </div>
@@ -509,7 +743,7 @@
<div class="search-section"> <div class="search-section">
<h2 style="margin-bottom: 15px; color: #ffffff;">Search Aliases</h2> <h2 style="margin-bottom: 15px; color: #ffffff;">Search Aliases</h2>
<div class="search-container"> <div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Search for alias or destination email..."> <input type="text" class="search-input" id="searchInput" placeholder="Start typing alias address (e.g., power...)">
<button class="btn btn-primary" onclick="performSearch()">Search</button> <button class="btn btn-primary" onclick="performSearch()">Search</button>
</div> </div>
</div> </div>
@@ -578,17 +812,53 @@
</div> </div>
</div> </div>
<!-- Delete Alias Modal --> <!-- Delete Aliases Modal -->
<div class="config-modal" id="deleteAliasModal"> <div class="config-modal" id="deleteAliasModal">
<div class="config-content"> <div class="config-content" style="max-width: 800px;">
<h2>Delete Alias</h2> <h2>Delete Aliases</h2>
<!-- Search Section -->
<div class="form-group"> <div class="form-group">
<label for="deleteAliasEmail">Alias Email to Delete</label> <label for="deleteSearchInput">Search for aliases to delete</label>
<input type="text" id="deleteAliasEmail" placeholder="alias@example.com"> <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> </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"> <div class="modal-buttons">
<button class="btn" onclick="closeDeleteAlias()">Cancel</button> <button class="btn" onclick="closeDeleteAliasesModal()">Cancel</button>
<button class="btn btn-primary" onclick="deleteAlias()">Delete</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>
<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>
</div> </div>
</div> </div>
</div> </div>
@@ -876,23 +1146,258 @@
} }
} }
// Delete Alias Modal Functions // Delete Aliases Modal Functions
function openDeleteAlias() { let selectedAliases = new Set();
let currentDeleteResults = [];
function openDeleteAliasesModal() {
document.getElementById('deleteAliasModal').classList.add('active'); document.getElementById('deleteAliasModal').classList.add('active');
document.getElementById('deleteSearchInput').value = '';
document.getElementById('deleteResultsSection').style.display = 'none';
selectedAliases.clear();
currentDeleteResults = [];
updateDeleteButtonText();
} }
function closeDeleteAlias() { function closeDeleteAliasesModal() {
document.getElementById('deleteAliasModal').classList.remove('active'); document.getElementById('deleteAliasModal').classList.remove('active');
selectedAliases.clear();
currentDeleteResults = [];
} }
async function deleteAlias() { async function searchAliasesForDeletion() {
const alias = document.getElementById('deleteAliasEmail').value; const query = document.getElementById('deleteSearchInput').value;
if (!alias) { if (!query.trim()) {
showResult('Alias email is required', false); showResult('Please enter a search query', false);
return; 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 { try {
const response = await fetch('/delete_alias', { const response = await fetch('/delete_alias', {
method: 'POST', method: 'POST',
@@ -902,17 +1407,41 @@
body: JSON.stringify({ alias: alias }) body: JSON.stringify({ alias: alias })
}); });
if (checkAuth(response)) return;
const data = await response.json(); const data = await response.json();
showResult(data.message, data.status === 'success');
if (data.status === 'success') { if (data.status === 'success') {
document.getElementById('deleteAliasEmail').value = ''; // Remove from selected if it was selected
closeDeleteAlias(); 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);
} }
} catch (error) { } catch (error) {
showResult(`Error deleting alias: ${error.message}`, false); 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 // Timed Alias Modal Functions
function openTimedAlias() { function openTimedAlias() {
document.getElementById('timedAliasModal').classList.add('active'); document.getElementById('timedAliasModal').classList.add('active');
@@ -960,15 +1489,14 @@
if (e.target === this) closeCreateAlias(); if (e.target === this) closeCreateAlias();
}); });
document.getElementById('deleteAliasModal').addEventListener('click', function(e) { document.getElementById('deleteAliasModal').addEventListener('click', function(e) {
if (e.target === this) closeDeleteAlias(); if (e.target === this) closeDeleteAliasesModal();
});
document.getElementById('deleteConfirmModal').addEventListener('click', function(e) {
if (e.target === this) closeDeleteConfirm();
}); });
document.getElementById('timedAliasModal').addEventListener('click', function(e) { document.getElementById('timedAliasModal').addEventListener('click', function(e) {
if (e.target === this) closeTimedAlias(); if (e.target === this) closeTimedAlias();
}); });
</script> </script>
</body> </body>
<div class="footer">
<p style="padding-top:2%">&copy; <a href="https://opensource.org/license/mit">MIT</a> 2026 Rune Olsen <a href="https://blog.rune.pm">Blog</a> &#8212; <a href="https://gitlab.pm/rune/malias-web">Source Code</a></p>
</div>
</html> </html>