diff --git a/install.sh b/install.sh index 970eca1..6c06ee3 100644 --- a/install.sh +++ b/install.sh @@ -1,13 +1,13 @@ #!/bin/bash #================================================================================= -# M365 Mailbox Backup Tool - Automatic Installation Script (v2.0 - Final English) +# M365 Mailbox Backup Tool - Automatic Installation Script (v2.1 - Interactive IP) #================================================================================= # --- Color Codes --- GREEN='\033[0;32m' RED='\033[0;31m' -NC='\033[0m' +NC='\033[0m' # No Color if [ "$EUID" -ne 0 ]; then echo -e "${RED}Please run this script with 'sudo': sudo ./install.sh${NC}" @@ -16,12 +16,16 @@ fi echo "=== M365 Backup Tool Installation Started ===" -# --- User Input Block --- -IP_ADDR=$(hostname -I | awk '{print $1}') +# --- NEW: INTERACTIVE IP PROMPT --- +DETECTED_IP=$(hostname -I | awk '{print $1}') +read -p "Enter the server IP address or press Enter to use the detected IP [$DETECTED_IP]: " USER_IP +IP_ADDR=${USER_IP:-$DETECTED_IP} +# ------------------------------------ + REDIRECT_URI="https://$IP_ADDR/get_token" echo "---------------------------------------------------------------------" -echo "IMPORTANT: Please go to your Azure AD App Registration and add the following" -echo "Redirect URI under the 'Web' platform section:" +echo "IMPORTANT: Please ensure you have set the following Redirect URI" +echo "in your Azure AD App Registration under the 'Web' platform section:" echo "" echo "$REDIRECT_URI" echo "" @@ -35,13 +39,13 @@ read CLIENT_ID echo -n "Enter your Client Secret Value (will not be visible): " read -s CLIENT_SECRET echo "" -echo -n "Enter comma-separated emails of allowed admins (e.g., admin1@company.com): " +echo -n "Enter comma-separated emails of allowed admins: " read ALLOWED_ADMINS echo "" echo "Credentials received." # --- System Dependency Installation --- -echo "--> Updating system and installing required packages (nginx, redis, python3-venv)..." +echo "--> Updating system and installing required packages..." apt-get update > /dev/null 2>&1 apt-get install -y nginx redis-server python3-pip python3-venv > /dev/null 2>&1 @@ -62,8 +66,6 @@ pip install Flask gunicorn "celery[redis]" msal requests python-decouple > /dev/ deactivate # --- Application File Creation --- - -# .env file echo "--> Creating .env file for credentials..." cat < .env TENANT_ID=${TENANT_ID} @@ -74,8 +76,8 @@ ALLOWED_ADMINS=${ALLOWED_ADMINS} REDIRECT_URI=${REDIRECT_URI} EOF +echo "--> Creating application files and templates..." # app.py file -echo "--> Creating main application file (app.py)..." cat <<'EOF' > app.py import os, re, msal, requests, json, secrets from flask import Flask, request, redirect, url_for, render_template, send_from_directory, session @@ -86,7 +88,6 @@ from datetime import datetime, timedelta from decouple import config from functools import wraps -# --- CONFIGURATION --- TENANT_ID = config('TENANT_ID', default='') CLIENT_ID = config('CLIENT_ID', default='') CLIENT_SECRET = config('CLIENT_SECRET', default='') @@ -115,8 +116,7 @@ def login_required(f): return decorated_function @app.route('/login_page') -def show_login_page(): - return render_template('login.html') +def show_login_page(): return render_template('login.html') @app.route('/login') def login(): @@ -139,8 +139,7 @@ def get_token(): @app.route('/logout') def logout(): session.clear() - logout_uri = f"https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url_for('show_login_page', _external=True, _scheme='https')}" - return redirect(logout_uri) + return redirect(f"https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri={url_for('show_login_page', _external=True, _scheme='https')}") def sanitize_filename(filename): return re.sub(r'[\\/*?:"<>|]', "", filename) @@ -191,11 +190,9 @@ def trigger_backup(user_email, date_option='all_time', start_date_str=None, end_ messages_url = messages_data.get('@odata.nextLink') total_message_count += message_count_in_folder return f"Total {total_message_count} emails downloaded." - @app.route('/') @login_required def index(): return render_template('index.html') - @app.route('/get_users') @login_required def get_users(): @@ -209,7 +206,6 @@ def get_users(): if response.status_code != 200: return render_template('partials/user_list_error.html', error_message="Error fetching users.") data = response.json(); users.extend(data.get('value', [])); users_url = data.get('@odata.nextLink') return render_template('partials/user_list.html', users=sorted(users, key=lambda u: u.get('displayName') or '')) - @app.route('/backup', methods=['POST']) @login_required def backup(): @@ -220,7 +216,6 @@ def backup(): session['tasks'].append({'id': task.id, 'email': email}) session.modified = True return redirect(url_for('list_tasks')) - @app.route('/tasks') @login_required def list_tasks(): @@ -229,13 +224,11 @@ def list_tasks(): task = trigger_backup.AsyncResult(task_meta.get('id')) task_info.append({'id': task_meta.get('id'), 'email': task_meta.get('email'), 'status': task.state, 'result': task.result if task.state in ['SUCCESS', 'FAILURE'] else str(task.info)}) return render_template('tasks.html', tasks=reversed(task_info)) - @app.route('/backups') @login_required def list_backups(): if not os.path.exists(BACKUP_DIR): os.makedirs(BACKUP_DIR) return render_template('backups.html', users=os.listdir(BACKUP_DIR)) - @app.route('/backups/') @login_required def restore_portal(user_email): @@ -259,12 +252,9 @@ def restore_portal(user_email): if search_query not in content_to_search.lower(): continue emails.append({'filename': filename, 'subject': msg['subject'], 'from': msg['from'], 'date': msg['date']}) return render_template('portal.html', user_email=user_email, folders=sorted(all_folders), emails=emails, active_folder=active_folder_name, search_query=search_query) - @app.route('/download///') @login_required -def download_file(user_email, folder_name, filename): - return send_from_directory(directory=os.path.join(BACKUP_DIR, user_email, folder_name), path=filename, as_attachment=True) - +def download_file(user_email, folder_name, filename): return send_from_directory(directory=os.path.join(BACKUP_DIR, user_email, folder_name), path=filename, as_attachment=True) @app.route('/preview///') @login_required def preview_file(user_email, folder_name, filename): @@ -283,18 +273,13 @@ def preview_file(user_email, folder_name, filename): else: return {"body": "Could not display content."} EOF -# --- Template File Creation --- -echo "--> Creating HTML templates..." mkdir -p templates/partials -# login.html cat <<'EOF' > templates/login.html Login - M365 Backup Tool
EOF -# layout.html cat <<'EOF' > templates/layout.html {% block title %}{% endblock %} - M365 Backup Tool

{% block content %}{% endblock %}
EOF -# index.html cat <<'EOF' > templates/index.html {% extends 'layout.html' %}{% block title %}New Backup{% endblock %}{% block content %}

Start New Backup


Select users to back up:


Loading user list from Azure...

{% endblock %} EOF -# partials/user_list.html cat <<'EOF' > templates/partials/user_list.html
{% for user in users %} {% else %}{% endfor %}
SelectDisplay NameEmail
{{ user.displayName }}{{ user.userPrincipalName }}
No users found or error loading users.
EOF -# partials/user_list_error.html cat <<'EOF' > templates/partials/user_list_error.html
Error Loading Users

Could not load the user list.

Details: {{ error_message }}

EOF -# backups.html cat <<'EOF' > templates/backups.html {% extends 'layout.html' %}{% block title %}Existing Backups{% endblock %}{% block content %}

Backed Up Users

    {% for user in users %}
  • {{ user }}
  • {% else %}
  • No backups found yet.
  • {% endfor %}
{% endblock %} EOF -# portal.html cat <<'EOF' > templates/portal.html {% extends 'layout.html' %}{% block title %}{{ user_email }} - Restore Portal{% endblock %}{% block content %} @@ -340,7 +321,6 @@ iframe.contentWindow.document.open();iframe.contentWindow.document.write(data.bo } {% endblock %} EOF -# tasks.html cat <<'EOF' > templates/tasks.html {% extends 'layout.html' %}{% block title %}Task Status{% endblock %}{% block content %}

Task Status

{% for task in tasks %} @@ -348,15 +328,13 @@ cat <<'EOF' > templates/tasks.html
Task IDUserStatusResult
{{ task.id }}{{ task.email }}{{ task.status }}{{ task.result }}
Refresh Page{% endblock %} EOF -# --- SSL Certificate Creation --- echo "--> Creating self-signed SSL certificate..." mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/m365-backup.key \ -out /etc/nginx/ssl/m365-backup.crt \ - -subj "/C=AZ/ST=Baku/L=Baku/O=DevOps/OU=IT/CN=$IP_ADDR" + -subj "/C=XX/ST=Global/L=Global/O=Global/OU=IT/CN=$IP_ADDR" -# --- Service File Creation --- echo "--> Creating systemd service files..." cat < /etc/systemd/system/m365-web.service [Unit] @@ -386,7 +364,6 @@ Restart=always WantedBy=multi-user.target EOF -# --- Nginx Configuration --- echo "--> Creating Nginx proxy configuration..." cat < /etc/nginx/sites-available/m365-backup server { @@ -400,7 +377,6 @@ server { ssl_certificate /etc/nginx/ssl/m365-backup.crt; ssl_certificate_key /etc/nginx/ssl/m365-backup.key; ssl_protocols TLSv1.2 TLSv1.3; - location / { include proxy_params; proxy_pass http://unix:$PROJECT_DIR/m365-backup.sock; @@ -410,11 +386,9 @@ EOF rm -f /etc/nginx/sites-enabled/default ln -sf /etc/nginx/sites-available/m365-backup /etc/nginx/sites-enabled/ -# --- Folder Permissions --- chown -R $USERNAME:www-data "$PROJECT_DIR" chmod -R 775 "$PROJECT_DIR" -# --- Service Start and Verification --- echo "--> Starting and enabling all services..." systemctl daemon-reload systemctl restart redis-server nginx @@ -433,7 +407,6 @@ if systemctl is-active --quiet m365-worker.service; then else echo -e "${RED}❌ Background Worker (Celery) failed to start!${NC}"; FINAL_STATUS=1; fi -# --- Final Message --- echo "" echo "============================================================" if [ $FINAL_STATUS -eq 0 ]; then