main
SundayEnglish 5 months ago
parent 31f7ee98c0
commit 2bc46325a5
  1. 79
      app.py
  2. 20
      data/services.json
  3. 77
      static/script.js
  4. 39
      templates/index.html
  5. 30
      templates/login.html

@ -1,39 +1,88 @@
from flask import Flask, render_template, request, jsonify
import subprocess
import json
import os
import time
from flask import Flask, render_template, request, Response, jsonify, redirect, url_for, session
from datetime import timedelta
app = Flask(__name__)
app.secret_key = "your_secret_key"
app.permanent_session_lifetime = timedelta(days=1)
# Danh sách tài khoản & mật khẩu
USERS = {
"admin": "123456",
"user1": "password1",
"user2": "password2"
}
# Định nghĩa đường dẫn file JSON chứa danh sách service
SERVICES_FILE = os.path.join(os.path.dirname(__file__), "data", "services.json")
# Load danh sách dịch vụ từ file JSON
def load_services():
with open(SERVICES_FILE, "r", encoding="utf-8") as file:
return json.load(file)
@app.route("/")
def home():
listService = load_services() # Đọc danh sách service từ file
if "user" not in session:
return redirect(url_for("login"))
listService = load_services()
return render_template("index.html", listService=listService)
@app.route("/run-command", methods=["POST"])
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if username in USERS and USERS[username] == password:
session["user"] = username
session.permanent = True
return redirect(url_for("home"))
else:
return render_template("login.html", error="Invalid username or password")
return render_template("login.html")
@app.route("/logout")
def logout():
session.pop("user", None)
return redirect(url_for("login"))
# Streaming real-time output của lệnh
def generate_output(command_list):
# Chuyển danh sách thành chuỗi để chạy trong shell
command_str = " ".join(command_list)
process = subprocess.Popen(command_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, executable="/bin/bash")
for line in iter(process.stdout.readline, ''):
yield f"data: {line.strip()}\n\n"
time.sleep(0.1)
for line in iter(process.stderr.readline, ''):
yield f"data: {line.strip()}\n\n"
time.sleep(0.1)
process.wait()
if process.returncode == 0:
yield f"data: SUCCESS\n\n"
else:
yield f"data: ERROR\n\n"
@app.route("/run-command", methods=["GET"])
def run_command():
data = request.json
command = data.get("command")
if "user" not in session:
return jsonify({"error": "Unauthorized"}), 401
if not command:
return jsonify({"error": "No command provided"}), 400
command_list = request.args.getlist("command[]") # Nhận danh sách lệnh từ frontend
try:
# Chạy lệnh trong shell và lấy kết quả đầu ra
result = subprocess.run(command, shell=True, capture_output=True, text=True, executable="/bin/bash")
output = result.stdout.strip() if result.returncode == 0 else result.stderr.strip()
if not command_list:
return jsonify({"error": "No command provided"}), 400
return jsonify({"status": "success" if result.returncode == 0 else "error", "output": output})
except Exception as e:
return jsonify({"status": "error", "output": str(e)}), 500
return Response(generate_output(command_list), mimetype="text/event-stream")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

@ -1,4 +1,20 @@
[
{"name": "service.sundayenglish.com", "command": "cd /project/service-se && ls"},
{"name": "exercise.sundayenglish.com", "command": "cd /project/exercise-se && ls"}
{
"name": "Service",
"url": "http://service.sundayenglish.com",
"description": "Sunday English Service",
"command": ["cd", "/projects/service-se", "&&", "ls"]
},
{
"name": "Exercise",
"url": "http://exercise.sundayenglish.com",
"description": "Sunday English Exercise",
"command": ["cd", "/projects/exercise-se", "&&", "ls"]
},
{
"name": "Julius",
"url": null,
"description": "Julius",
"command": ["cd", "/projects/julius", "&&", "docker", "compose", "down", "&&", "docker", "compose", "up", "-d"]
}
]

@ -7,47 +7,76 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
function runCommand(button, command) {
function runCommand(button, commandStr) {
const originalText = button.innerHTML;
button.innerHTML =
'<span class="spinner-border spinner-border-sm"></span> Running...';
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Running...';
button.disabled = true;
fetch("/run-command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ command: command }),
})
.then((response) => response.json())
.then((data) => {
// Kiểm tra xem lệnh có chạy thành công không
if (data.output) {
showToast("Success", "success"); // Hiển thị thông báo "Success"
const outputContainer = document.getElementById("commandOutput");
outputContainer.innerHTML = ""; // Xóa output cũ
// Chuyển đổi chuỗi JSON thành mảng lệnh
let command;
try {
command = JSON.parse(commandStr);
} catch (error) {
showToast("Invalid command format", "danger");
button.innerHTML = originalText;
button.disabled = false;
return;
}
// Kiểm tra nếu command không phải là mảng
if (!Array.isArray(command)) {
showToast("Command must be an array", "danger");
button.innerHTML = originalText;
button.disabled = false;
return;
}
// Gửi danh sách command dưới dạng URL parameter
const url = new URL("/run-command", window.location.origin);
command.forEach(cmd => url.searchParams.append("command[]", cmd));
const eventSource = new EventSource(url);
eventSource.onmessage = function (event) {
if (event.data === "SUCCESS") {
showToast("Success", "success");
eventSource.close();
button.innerHTML = originalText;
button.disabled = false;
} else if (event.data === "ERROR") {
showToast("Error", "danger");
eventSource.close();
button.innerHTML = originalText;
button.disabled = false;
} else {
showToast("Error", "danger"); // Hiển thị thông báo "Error"
outputContainer.innerHTML += event.data + "\n";
outputContainer.scrollTop = outputContainer.scrollHeight;
}
})
.catch(() => {
showToast("Error", "danger"); // Hiển thị "Error" nếu request thất bại
})
.finally(() => {
};
eventSource.onerror = function () {
showToast("Error", "danger");
eventSource.close();
button.innerHTML = originalText;
button.disabled = false;
});
};
}
// Hàm hiển thị Toast chỉ với "Success" hoặc "Error"
function showToast(status, type) {
// Hiển thị Toast thông báo
function showToast(message, type) {
const toastContainer = document.getElementById("toastContainer");
const toast = document.createElement("div");
toast.className = `toast align-items-center text-bg-${type} border-0 show`;
toast.role = "alert";
toast.innerHTML = `<div class="toast-body text-center fw-bold">${status}</div>`;
toast.innerHTML = `<div class="toast-body text-center fw-bold">${message}</div>`;
toastContainer.appendChild(toast);
// Tự động ẩn sau 5 giây
setTimeout(() => {
toast.remove();
}, 5000);

@ -6,23 +6,30 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service List</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Load CSS từ thư mục static -->
<!-- Bootstrap 5.3 từ Cloudflare CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container p-4">
<h1 class="text-center text-white">🚀 Service List 🚀</h1>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="text-white">🚀 Service List 🚀</h1>
<div>
<span class="text-white me-3">👤 {{ session['user'] }}</span>
<a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered text-center">
<thead class="table-dark">
<tr>
<th>Service Name</th>
<th>Description</th>
<th>URL</th>
<th>Command</th>
<th>Action</th>
</tr>
@ -31,9 +38,18 @@
{% for service in listService %}
<tr>
<td><strong>{{ service.name }}</strong></td>
<td>{{ service.description }}</td>
<td>
{% if service.url %}
<a href="{{ service.url }}" target="_blank" class="btn btn-link">🔗 Visit</a>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td><code class="text-primary">{{ service.command }}</code></td>
<td>
<button class="btn btn-success btn-sm run-btn" data-command="{{ service.command }}">
<button class="btn btn-success btn-sm run-btn"
data-command='{{ service.command | tojson | safe }}'>
Run
</button>
</td>
@ -45,17 +61,16 @@
<!-- Hiển thị kết quả -->
<div class="mt-4">
<h5 class="text-white">Command Output:</h5>
<pre id="commandOutput" class="bg-dark text-white p-3 rounded"></pre>
<h5 class="text-white">Command Output (Real-time Logs):</h5>
<pre id="commandOutput" class="bg-dark text-white p-3 rounded" style="height: 300px; overflow-y: auto;"></pre>
</div>
</div>
<!-- Container hiển thị Toast -->
<div id="toastContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;"></div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Load JavaScript từ file tĩnh -->
<!-- Bootstrap JS từ Cloudflare CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
</head>
<body class="d-flex align-items-center justify-content-center vh-100 bg-dark text-white">
<div class="card p-4 shadow-lg" style="width: 350px;">
<h3 class="text-center">🔐 Login</h3>
{% if error %}
<div class="alert alert-danger text-center">{{ error }}</div>
{% endif %}
<form method="POST">
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" class="form-control" name="username" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</body>
</html>
Loading…
Cancel
Save