mirror of
https://github.com/ION606/static-site-hosting.git
synced 2026-05-14 22:16:54 +00:00
Split routes into multiple files (#2)
* split into files * attempted path fix * perms fix * added missed functions * fixed circular dependancy * I hate splitting * env fix * path fix
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from .scheduler import scheduler
|
||||
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuration
|
||||
app.config.from_object("app.config.Config")
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Initialize scheduler
|
||||
from .scheduler import init_scheduler
|
||||
|
||||
init_scheduler(app)
|
||||
|
||||
# Register blueprints
|
||||
from .routes import main_routes
|
||||
|
||||
app.register_blueprint(main_routes)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
from secrets import token_hex
|
||||
|
||||
|
||||
def get_secret_key():
|
||||
try:
|
||||
with open("/app/instance/secret.key", "rb") as f:
|
||||
return bytes.hex(f.readline())
|
||||
except FileNotFoundError as e:
|
||||
if not os.path.exists("/app/instance"):
|
||||
os.mkdir("/app/instance")
|
||||
|
||||
with open("/app/instance/secret.key", "wb") as f:
|
||||
newKey = token_hex(64)
|
||||
f.write(bytearray.fromhex(newKey))
|
||||
return newKey
|
||||
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = get_secret_key()
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:////app/instance/db.sqlite"
|
||||
UPLOAD_FOLDER = "/app/sites"
|
||||
SERVER_NAME = "tinysite.cloud"
|
||||
SESSION_COOKIE_DOMAIN = ".tinysite.cloud"
|
||||
SESSION_COOKIE_NAME = "tinysite_session"
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SAMESITE = "Lax"
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True, "pool_recycle": 300}
|
||||
PORT = 5121
|
||||
RESERVED_SUBDOMAINS = {
|
||||
"",
|
||||
"www",
|
||||
"api",
|
||||
"admin",
|
||||
"support",
|
||||
"docs",
|
||||
"blog",
|
||||
"cdn",
|
||||
"test",
|
||||
"dev",
|
||||
"staging",
|
||||
"secure",
|
||||
"mail",
|
||||
"status",
|
||||
"gateway",
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
from flask import current_app
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
def delete_inactive_sites():
|
||||
from .models import db, Site # lazy import to avoid circular dependency
|
||||
|
||||
with current_app.app_context():
|
||||
cutoff = datetime.utcnow() - timedelta(days=30)
|
||||
sites = Site.query.filter(Site.last_accessed < cutoff).all()
|
||||
for site in sites:
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(site.user_id), str(site.id)
|
||||
)
|
||||
if os.path.exists(site_dir):
|
||||
shutil.rmtree(site_dir)
|
||||
db.session.delete(site)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def list_files(directory):
|
||||
try:
|
||||
return os.listdir(directory)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
@@ -0,0 +1,25 @@
|
||||
from . import db
|
||||
from datetime import datetime
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(100), unique=True)
|
||||
password_hash = db.Column(db.String(128))
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
|
||||
class Site(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer)
|
||||
name = db.Column(db.String(100))
|
||||
subdomain = db.Column(db.String(100), unique=True)
|
||||
last_accessed = db.Column(db.DateTime)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
session,
|
||||
send_from_directory,
|
||||
abort,
|
||||
jsonify,
|
||||
current_app,
|
||||
)
|
||||
from flask_login import login_required, current_user, login_user, logout_user
|
||||
from werkzeug.exceptions import NotFound
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import shutil
|
||||
from .models import db, User, Site
|
||||
from .upload import handle_upload
|
||||
from . import login_manager
|
||||
from .helpers import list_files
|
||||
from .config import Config
|
||||
|
||||
main_routes = Blueprint("main", __name__, template_folder="templates")
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
@main_routes.app_context_processor
|
||||
def inject_utilities():
|
||||
return dict(list_files=list_files)
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
# Context processor
|
||||
@main_routes.app_context_processor
|
||||
def inject_subdomain():
|
||||
host = request.host
|
||||
server_parts = current_app.config["SERVER_NAME"].split(".")
|
||||
host_parts = host.split(".")
|
||||
|
||||
subdomain = None
|
||||
if host_parts[-len(server_parts) :] == server_parts:
|
||||
subdomain_parts = host_parts[: -len(server_parts)]
|
||||
if subdomain_parts:
|
||||
subdomain = ".".join(subdomain_parts)
|
||||
|
||||
return {
|
||||
"SUBDOMAIN": subdomain,
|
||||
"FULL_DOMAIN": host,
|
||||
"SERVERNAME": current_app.config["SERVER_NAME"],
|
||||
}
|
||||
|
||||
|
||||
# Route definitions
|
||||
@main_routes.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
session.permanent = True
|
||||
return redirect(url_for("main.dashboard"))
|
||||
flash("Invalid email or password")
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@main_routes.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("main.home"))
|
||||
|
||||
|
||||
@main_routes.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash("Email already exists")
|
||||
return redirect(url_for("main.register"))
|
||||
|
||||
new_user = User(email=email)
|
||||
new_user.set_password(password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
login_user(new_user)
|
||||
return redirect(url_for("main.dashboard"))
|
||||
return render_template("register.html")
|
||||
|
||||
|
||||
@main_routes.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
sites = Site.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
sites=sites,
|
||||
subdomain=request.host.split(".")[0],
|
||||
hostname=current_app.config["SERVER_NAME"],
|
||||
)
|
||||
|
||||
|
||||
@main_routes.route("/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload_site():
|
||||
site_name = request.form.get("name")
|
||||
subdomain = request.form.get("subdomain").strip().lower()
|
||||
files = request.files.getlist("files")
|
||||
return handle_upload(current_user, site_name, subdomain, files)
|
||||
|
||||
|
||||
@main_routes.route("/edit/<int:site_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_site(site_id):
|
||||
site = Site.query.get_or_404(site_id)
|
||||
if site.user_id != current_user.id:
|
||||
return "Unauthorized", 403
|
||||
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id)
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
site.name = request.form.get("name", site.name)
|
||||
db.session.commit()
|
||||
|
||||
for filename, content in request.form.items():
|
||||
if filename.endswith((".html", ".css", ".js")):
|
||||
filepath = os.path.join(site_dir, filename)
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return jsonify({"success": True, "message": "Site updated successfully!"})
|
||||
|
||||
flash("Site updated successfully!", "success")
|
||||
return redirect(url_for("main.edit_site", site_id=site.id))
|
||||
|
||||
files = {}
|
||||
for file in os.listdir(site_dir):
|
||||
with open(os.path.join(site_dir, file), "r") as f:
|
||||
files[file] = f.read()
|
||||
|
||||
return render_template("edit.html", site=site, files=files)
|
||||
|
||||
|
||||
@main_routes.route("/delete/<int:site_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_site(site_id):
|
||||
site = Site.query.get_or_404(site_id)
|
||||
if site.user_id != current_user.id:
|
||||
return "Unauthorized", 403
|
||||
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id)
|
||||
)
|
||||
if os.path.exists(site_dir):
|
||||
shutil.rmtree(site_dir)
|
||||
|
||||
db.session.delete(site)
|
||||
db.session.commit()
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
|
||||
@main_routes.route("/delete_file/<int:site_id>/<filename>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_file(site_id, filename):
|
||||
site = Site.query.get_or_404(site_id)
|
||||
if site.user_id != current_user.id:
|
||||
return "Unauthorized", 403
|
||||
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id)
|
||||
)
|
||||
file_path = os.path.join(site_dir, filename)
|
||||
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
flash(f"File '{filename}' deleted successfully!", "success")
|
||||
else:
|
||||
flash(f"File '{filename}' not found!", "error")
|
||||
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
|
||||
@main_routes.route(
|
||||
"/",
|
||||
subdomain="<subdomain>",
|
||||
defaults={"subdomain": ""},
|
||||
)
|
||||
@main_routes.route("/<path:filename>", subdomain="<subdomain>")
|
||||
@main_routes.route("/<path:filename>")
|
||||
def serve_site_content(filename="index.html", subdomain=""):
|
||||
if is_default_route(request.host):
|
||||
abort(404)
|
||||
|
||||
# no subdomain provided via route --> extract from host header
|
||||
if not subdomain:
|
||||
server_parts = current_app.config["SERVER_NAME"].split('.')
|
||||
host_parts = request.host.split('.')
|
||||
if host_parts[-len(server_parts):] == server_parts and len(host_parts) > len(server_parts):
|
||||
subdomain = ".".join(host_parts[:-len(server_parts)])
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
site = Site.query.filter_by(subdomain=subdomain).first_or_404()
|
||||
site.last_accessed = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(site.user_id), str(site.id)
|
||||
)
|
||||
|
||||
if ".." in filename or filename.startswith("/") or not os.path.exists(site_dir):
|
||||
abort(404)
|
||||
|
||||
try:
|
||||
return send_from_directory(site_dir, filename)
|
||||
except NotFound:
|
||||
if "." not in filename:
|
||||
try:
|
||||
return send_from_directory(site_dir, f"{filename}.html")
|
||||
except NotFound:
|
||||
return send_from_directory(site_dir, "index.html")
|
||||
abort(404)
|
||||
|
||||
|
||||
@main_routes.route("/")
|
||||
def home():
|
||||
if not is_default_route(request.host):
|
||||
abort(404)
|
||||
return render_template("home.html")
|
||||
|
||||
|
||||
# Helper functions
|
||||
def is_default_route(hostname: str):
|
||||
if hostname == current_app.config["SERVER_NAME"]:
|
||||
return True
|
||||
|
||||
parts = hostname.split(".")
|
||||
server_parts = current_app.config["SERVER_NAME"].split(".")
|
||||
|
||||
if parts[-len(server_parts) :] != server_parts:
|
||||
return False
|
||||
|
||||
subdomain = ".".join(parts[: -len(server_parts)])
|
||||
return (
|
||||
any(part in Config.RESERVED_SUBDOMAINS for part in subdomain.split("."))
|
||||
or subdomain in Config.RESERVED_SUBDOMAINS
|
||||
)
|
||||
|
||||
|
||||
# Error handler (would typically be registered in current_app factory)
|
||||
@main_routes.app_errorhandler(404)
|
||||
def page_not_found(_):
|
||||
print("404", request.host)
|
||||
host = request.host
|
||||
server_name = current_app.config["SERVER_NAME"]
|
||||
server_parts = server_name.split(".")
|
||||
host_parts = host.split(".")
|
||||
show_domain = False
|
||||
|
||||
if host == server_name:
|
||||
show_domain = True
|
||||
else:
|
||||
if host_parts[-len(server_parts) :] == server_parts:
|
||||
subdomain = ".".join(host_parts[: -len(server_parts)])
|
||||
if subdomain and subdomain not in Config.RESERVED_SUBDOMAINS:
|
||||
if not Site.query.filter_by(subdomain=subdomain).first():
|
||||
show_domain = True
|
||||
|
||||
return (
|
||||
render_template(
|
||||
"404.html",
|
||||
domain=host if show_domain else None,
|
||||
is_main_domain=(host == server_name),
|
||||
),
|
||||
404,
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
# app/scheduler.py
|
||||
from flask_apscheduler import APScheduler
|
||||
from .helpers import delete_inactive_sites
|
||||
|
||||
scheduler = APScheduler()
|
||||
|
||||
|
||||
def init_scheduler(app):
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
scheduler.add_job(
|
||||
id="delete_job",
|
||||
func=delete_inactive_sites,
|
||||
trigger="interval",
|
||||
days=1,
|
||||
)
|
||||
@@ -0,0 +1,155 @@
|
||||
function formatOption(option) {
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
const $option = $(`
|
||||
<div class="custom-option">
|
||||
<span class="option-text">${option.text}</span>
|
||||
</div>
|
||||
`);
|
||||
return $option;
|
||||
}
|
||||
|
||||
|
||||
function getAceMode(language) {
|
||||
let aceMode = "ace/mode/html"; // default fallback
|
||||
if (language === "css") {
|
||||
aceMode = "ace/mode/css";
|
||||
} else if (language === "javascript") {
|
||||
aceMode = "ace/mode/javascript";
|
||||
} else if (language === "htmlmixed") {
|
||||
aceMode = "ace/mode/html";
|
||||
}
|
||||
return aceMode;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const storedTheme = localStorage.getItem('editortheme');
|
||||
|
||||
// Initialize Ace editor instance on the container
|
||||
const editor = ace.edit("editor-container");
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
|
||||
// Default mode as html mixed. It will be updated based on file type.
|
||||
editor.session.setMode("ace/mode/html");
|
||||
|
||||
// To track the current file (index)
|
||||
var currentFileIndex = null;
|
||||
|
||||
// Returns Ace mode based on file extension
|
||||
function getAceMode(filename) {
|
||||
if (filename.endsWith('.css')) return "ace/mode/css";
|
||||
if (filename.endsWith('.js')) return "ace/mode/javascript";
|
||||
return "ace/mode/html";
|
||||
}
|
||||
|
||||
// When a file card is clicked then load its content into the editor
|
||||
document.querySelectorAll('#file-list .file-card').forEach((card) => {
|
||||
card.addEventListener('click', () => {
|
||||
// Save changes of currently open file
|
||||
if (currentFileIndex !== null) document.getElementById(`textarea-${currentFileIndex}`).value = editor.getValue();
|
||||
currentFileIndex = card.getAttribute("data-index");
|
||||
|
||||
// Get filename and load content from corresponding hidden textarea
|
||||
const filename = card.getAttribute("data-file");
|
||||
editor.setValue(document.getElementById(`textarea-${currentFileIndex}`).value);
|
||||
editor.session.setMode(getAceMode(filename));
|
||||
|
||||
// Show the editor container
|
||||
document.getElementById("editor-container").classList.add("visible");
|
||||
});
|
||||
});
|
||||
|
||||
// Update the corresponding hidden textarea whenever the editor value changes
|
||||
editor.session.on("change", function (e) {
|
||||
if (currentFileIndex !== null) {
|
||||
const ta = document.getElementById("textarea-" + currentFileIndex);
|
||||
ta.value = editor.getValue();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Select2 for the theme selector
|
||||
$(document).ready(function () {
|
||||
$('#theme-selector').select2({
|
||||
templateResult: formatOption,
|
||||
placeholder: 'Select a theme',
|
||||
allowClear: true
|
||||
}).on('change', function () {
|
||||
const selectedTheme = $('#theme-selector').val();
|
||||
localStorage.setItem('editortheme', selectedTheme);
|
||||
editor.setTheme(selectedTheme);
|
||||
});
|
||||
});
|
||||
|
||||
// Retrieve the list of available themes
|
||||
const ThemeList = ace.require("ace/ext/themelist"),
|
||||
themes = ThemeList.themes,
|
||||
themeSelector = document.querySelector('#theme-selector');
|
||||
|
||||
themes.forEach((theme) => {
|
||||
var option = document.createElement('option');
|
||||
option.value = theme.theme;
|
||||
option.textContent = theme.caption;
|
||||
themeSelector.appendChild(option);
|
||||
});
|
||||
|
||||
if (storedTheme) themeSelector.value = storedTheme;
|
||||
|
||||
// Initialize Select2 on the theme selector
|
||||
$(themeSelector).select2({
|
||||
templateResult: formatOption,
|
||||
placeholder: 'Select a theme',
|
||||
allowClear: true
|
||||
}).on('change', function (_) {
|
||||
const selectedTheme = $('#theme-selector').val();
|
||||
localStorage.setItem('editortheme', selectedTheme);
|
||||
editor.setTheme(selectedTheme);
|
||||
});
|
||||
|
||||
// Add event listener for Ctrl+S to save the current file
|
||||
//TODO: save this
|
||||
const editEl = document.querySelector('#editor-container');
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
e.preventDefault();
|
||||
if (currentFileIndex !== null) {
|
||||
document.getElementById("textarea-" + currentFileIndex).value = editor.getValue();
|
||||
document.querySelector("form").submit();
|
||||
}
|
||||
}
|
||||
else if (editEl.contains(e.target)) {
|
||||
document.getElementById("textarea-" + currentFileIndex).value = editor.getValue();
|
||||
}
|
||||
});
|
||||
|
||||
// form submit intercept
|
||||
document.querySelector("form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
document.querySelectorAll("[id^='textarea-']").forEach((textarea) => {
|
||||
if (textarea.dataset.edited === "true") {
|
||||
formData.append(textarea.name, textarea.value);
|
||||
delete textarea.dataset.edited;
|
||||
}
|
||||
});
|
||||
|
||||
await fetch(window.location.href, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
alert("Site updated successfully!"); // Or display a flash message
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,342 @@
|
||||
:root {
|
||||
/* Light mode variables */
|
||||
--bg-color: #f0f2f5;
|
||||
--text-color: #333;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #ddd;
|
||||
--hover-bg: #f5f5f5;
|
||||
--accent-color: #1abc9c;
|
||||
--danger-color: #e74c3c;
|
||||
--success-color: #155724;
|
||||
--error-color: #721c24;
|
||||
--nav-bg: #2c3e50;
|
||||
--nav-text: #ffffff;
|
||||
--input-bg: #ffffff;
|
||||
--input-text: #333;
|
||||
--input-border: #ccc;
|
||||
--file-icon-brightness: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark mode variables */
|
||||
--bg-color: #121212;
|
||||
--text-color: #e0e0e0;
|
||||
--card-bg: #1f1f1f;
|
||||
--border-color: #333;
|
||||
--hover-bg: #2d2d2d;
|
||||
--accent-color: #1abc9c;
|
||||
--danger-color: #c0392b;
|
||||
--success-color: #d4edda;
|
||||
--error-color: #f8d7da;
|
||||
--nav-bg: #1f1f1f;
|
||||
--nav-text: #e0e0e0;
|
||||
--input-bg: #1f1f1f;
|
||||
--input-text: #e0e0e0;
|
||||
--input-border: #444;
|
||||
--file-icon-brightness: 0.8;
|
||||
}
|
||||
|
||||
/* General Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Navigation Bar */
|
||||
.navbar {
|
||||
background-color: var(--nav-bg);
|
||||
color: var(--nav-text);
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
color: var(--nav-text);
|
||||
text-decoration: none;
|
||||
margin-right: 1.5rem;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Forms and Inputs */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: var(--accent-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px rgba(26, 188, 156, 0.5);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button,
|
||||
.btn {
|
||||
padding: 10px 18px;
|
||||
margin: 5px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
background-color: var(--accent-color);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.btn:hover {
|
||||
background-color: #16a085;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* File Upload and Preview */
|
||||
.file-upload-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-upload-button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: filter 0.3s;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-upload-button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
margin-top: 15px;
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
position: relative;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 10px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
filter: brightness(var(--file-icon-brightness));
|
||||
}
|
||||
|
||||
.file-icon.html {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='%23e34f26' d='M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm308.2 127.9H124.4l4.1 49.4h175.6l-13.6 148.4-97.9 27v.3h-1.1l-98.7-27.3-6-75.8h47.7L138 320l53.5 14.5 53.7-14.5 6-62.2H84.3L71.5 112.2h241.1l-4.4 47.7z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.file-icon.css {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath fill='%232649ad' d='M0 32l34.9 395.8L192 480l157.1-52.2L384 32H0zm313.1 80l-4.8 47.3L193 208.9l-.3.1h111.5l-12.8 146.6-98.2 28.7-98.8-29.2-6.4-73.9h48.9l3.2 38.3 52.6 13.3 54.7-15.4 3.7-61.6-119.3-.3-2.2-24.7L237.6 138H0l22.8-31.3L64.1 64H320l-6.9 48z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.file-icon.js {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill='%23f7df1e' d='M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM243.8 381.4c0 43.6-25.6 63.5-62.9 63.5-33.7 0-53.2-17.4-63.2-38.5l34.3-20.7c6.6 11.7 12.6 21.6 27.1 21.6 13.8 0 22.6-5.4 22.6-26.5V237.7h42.1v143.7zm99.6 63.5c-39.1 0-64.4-18.6-76.7-43l34.3-19.8c9 14.7 20.8 25.6 41.5 25.6 17.4 0 28.6-8.7 28.6-20.8 0-14.4-11.4-19.5-30.7-28l-10.5-4.5c-30.4-12.9-50.5-29.2-50.5-63.5 0-31.6 24.1-55.6 61.6-55.6 26.8 0 46 9.3 59.8 33.7L368 290c-7.2-12.9-15-18-27.1-18-12.3 0-20.1 7.8-20.1 18 0 12.6 7.8 17.7 25.9 25.6l10.5 4.5c35.8 15.3 55.9 31 55.9 66.2 0 37.8-29.8 58.6-69.7 58.6z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
word-break: break-word;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.file-close {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.file-close:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Site Cards */
|
||||
.site-card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.site-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.flash-messages {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.flash-success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d6e9c6;
|
||||
}
|
||||
|
||||
.flash-error {
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color: #ebccd1;
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
#theme-toggle {
|
||||
background-color: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#theme-toggle:hover {
|
||||
background-color: var(--hover-bg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
width: 150px !important;
|
||||
transition: background 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-label:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
#editor-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
margin-top: 20px;
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
display: none; /* Initially hidden */
|
||||
}
|
||||
|
||||
#editor-container.visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
display: block; /* Display when visible */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 100px 20px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.error-emoji {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
max-width: 600px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.alien {
|
||||
font-size: 5rem;
|
||||
margin: 2rem 0;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const emojis = ["👽", "🚀", "🛸", "🌌", "🌠", "🔭", "🪐", "🌍", "✨", "👾"];
|
||||
const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)];
|
||||
|
||||
const alienElement = document.querySelector(".alien");
|
||||
if (alienElement) {
|
||||
alienElement.textContent = randomEmoji;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="error-container">
|
||||
<div class="alien">👽</div>
|
||||
<h1>Houston, We Have a Problem!</h1>
|
||||
|
||||
<div class="error-message">
|
||||
<p>The site you're looking for doesn't exist... yet!</p>
|
||||
<p>But don't worry, this corner of the internet is just waiting for your creativity.</p>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if domain %}
|
||||
<a href="{{ url_for('main.dashboard', _external=True) }}" class="btn cta-button">
|
||||
🚀 Create {{ domain.split('.')[0]|default("Site", true) }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="auth-buttons">
|
||||
{% if is_main_domain %}
|
||||
<p>Start your web hosting journey today!</p>
|
||||
<a href="{{ url_for('main.register', _external=True) }}" class="btn cta-button">Sign Up</a>
|
||||
{% else %}
|
||||
<p>The page you requested doesn't exist on this site.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="auth-buttons">
|
||||
<p>Start your web hosting journey today!</p>
|
||||
<a href="{{ url_for('main.login', _external=True) }}" class="btn cta-button">Login</a>
|
||||
<a href="{{ url_for('main.register', _external=True) }}" class="btn cta-button">Sign Up</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 3rem;">
|
||||
<small>PS: If you were looking for someone else's site, maybe they forgot to launch it! 🚀</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description"
|
||||
content="ION Static Site Hosting offers effortless hosting and sharing of static sites with instant deployment, dark mode interface, and secure, private data handling!">
|
||||
<meta name="keywords"
|
||||
content="static site hosting, instant deployment, dark mode, secure hosting, private data, file management, blazing fast">
|
||||
<meta name="author" content="ION606">
|
||||
<meta property="og:title" content="ION Static Site Hosting">
|
||||
<meta property="og:description"
|
||||
content="Host and share your static sites effortlessly with ION Static Site Hosting. Enjoy instant deployment, a sleek dark mode interface, and secure, private data handling!">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='hosting.png') }}">
|
||||
<meta property="og:url" content="https://{{ SERVERNAME }}/">
|
||||
<meta name="twitter:card" content="{{ url_for('static', filename='hosting.png') }}">
|
||||
<meta name="twitter:title" content="ION Static Site Hosting">
|
||||
<meta name="twitter:description"
|
||||
content="Host and share your static sites effortlessly with ION Static Site Hosting. Enjoy instant deployment, a sleek dark mode interface, and secure, private data handling!">
|
||||
<meta name="twitter:image" content="{{ url_for('static', filename='hosting.png') }}">
|
||||
|
||||
<title>ION Static Site Hosting - {% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" id="theme-style">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeToggle = document.querySelector('#theme-toggle');
|
||||
const body = document.body;
|
||||
|
||||
// Get saved theme from localStorage
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
body.setAttribute('data-theme', savedTheme);
|
||||
|
||||
if (!savedTheme) localStorage.setItem('theme', savedTheme);
|
||||
|
||||
// Set initial button text
|
||||
themeToggle.textContent = savedTheme === 'dark' ? 'Light Mode' : 'Dark Mode';
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const isDark = body.getAttribute('data-theme') === 'dark';
|
||||
body.setAttribute('data-theme', isDark ? 'light' : 'dark');
|
||||
localStorage.setItem('theme', isDark ? 'light' : 'dark');
|
||||
themeToggle.textContent = isDark ? 'Dark Mode' : 'Light Mode';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="{{ url_for('main.home', _external=True) }}">Home</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('main.dashboard', _external=True) }}">Dashboard</a>
|
||||
<a href="{{ url_for('main.logout', _external=True) }}">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('main.login', _external=True) }}">Login</a>
|
||||
<a href="{{ url_for('main.register', _external=True) }}">Register</a>
|
||||
{% endif %}
|
||||
<button id="theme-toggle" class="btn">Toggle Dark Mode</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-message flash-{{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Your Sites</h2>
|
||||
|
||||
<h3>Create New Site</h3>
|
||||
<form method="POST" action="{{ url_for('main.upload_site') }}" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label>Site Name:</label>
|
||||
<input type="text" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Site URL:</label>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input type="text" name="subdomain" required pattern="[a-zA-Z0-9\-_]+"
|
||||
title="Letters, numbers, hyphens, and underscores only" value="{{ subdomain }}"
|
||||
style="flex: 1;">
|
||||
<span style="margin-left: 5px;">.{{ hostname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" onclick="document.querySelector('#file-input').click()">
|
||||
<label class="upload-label">
|
||||
Upload Files
|
||||
<input type="file" name="files" style="display: none;" id="file-input" multiple accept=".html,.css,.js"
|
||||
required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="file-preview" class="file-preview">
|
||||
<h5>Selected Files:</h5>
|
||||
<div id="file-grid" class="file-grid"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" style="margin-top: 20px;">Create Site</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-bottom: 30px;"></div>
|
||||
|
||||
<h1>My Sites</h1>
|
||||
|
||||
<div class="sites-list">
|
||||
{% for site in sites %}
|
||||
<div class="site-card">
|
||||
<h4>{{ site.name }}</h4>
|
||||
<p>Created: {{ site.created_at.strftime('%Y-%m-%d') }}</p>
|
||||
|
||||
<!-- Display uploaded files -->
|
||||
<div class="uploaded-files">
|
||||
<h5>Uploaded Files:</h5>
|
||||
<div class="file-grid">
|
||||
{% set site_dir = 'sites/' + current_user.id|string + '/' + site.id|string %}
|
||||
{% for file in list_files(site_dir) %}
|
||||
<div class="file-card">
|
||||
<div
|
||||
class="file-icon {{ 'html' if file.endswith('.html') else 'css' if file.endswith('.css') else 'js' }}">
|
||||
</div>
|
||||
<span class="file-name">{{ file }}</span>
|
||||
<form method="POST" action="{{ url_for('main.delete_file', site_id=site.id, filename=file) }}"
|
||||
style="display: inline;">
|
||||
<button type="submit" class="file-close">×</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Site Actions -->
|
||||
<div class="site-actions">
|
||||
<a href="{{ url_for('main.edit_site', site_id=site.id) }}" class="btn">Edit</a>
|
||||
<a href="{{ url_for('main.serve_site_content', filename='index.html', _external=True, subdomain=site.subdomain) }}">Visit
|
||||
{{ site.name }}
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('main.delete_site', site_id=site.id) }}" style="display: inline;">
|
||||
<button type="submit" class="btn btn-danger">Delete Site</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- for File Management -->
|
||||
<script>
|
||||
const fileInput = document.querySelector('#file-input');
|
||||
const fileGrid = document.querySelector('#file-grid');
|
||||
|
||||
function getFileIconClass(filename) {
|
||||
if (filename.endsWith('.html')) return 'html';
|
||||
if (filename.endsWith('.css')) return 'css';
|
||||
if (filename.endsWith('.js')) return 'js';
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
function updateFilePreview() {
|
||||
fileGrid.innerHTML = '';
|
||||
const files = Array.from(fileInput.files);
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileCard = document.createElement('div');
|
||||
fileCard.className = 'file-card';
|
||||
|
||||
fileCard.innerHTML = `
|
||||
<div class="file-icon ${getFileIconClass(file.name)}"></div>
|
||||
<span class="file-name">${file.name}</span>
|
||||
<button type="button" class="file-close" onclick="removeFile(${index})">×</button>
|
||||
`;
|
||||
|
||||
fileGrid.appendChild(fileCard);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
const files = Array.from(fileInput.files);
|
||||
files.splice(index, 1);
|
||||
|
||||
const newFileList = new DataTransfer();
|
||||
files.forEach(file => newFileList.items.add(file));
|
||||
fileInput.files = newFileList.files;
|
||||
|
||||
updateFilePreview();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', updateFilePreview);
|
||||
fileInput.addEventListener('change', () => {
|
||||
document.querySelector('#file-preview').style.display = fileInput.files.length ? 'block' : 'non'
|
||||
updateFilePreview();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Site{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Edit {{ site.name }}</h2>
|
||||
<a href="{{ url_for('main.serve_site_content', subdomain=site.subdomain) }}" class="btn" target="_blank">View Site</a>
|
||||
|
||||
<div style="margin-bottom: 30px;"></div>
|
||||
|
||||
<label for="theme-selector">Select Theme:</label>
|
||||
<select id="theme-selector" style="width: 200px;">
|
||||
<!-- Theme options will be populated here -->
|
||||
</select>
|
||||
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>Site Name:</label>
|
||||
<input type="text" name="name" value="{{ site.name }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Files:</label>
|
||||
<ul id="file-list" class="file-grid" style="padding-left: 0; list-style: none;">
|
||||
{% for filename, content in files.items() %}
|
||||
<li class="file-card" style="cursor: pointer;" data-index="{{ loop.index0 }}" data-file="{{ filename }}">
|
||||
<div class="file-icon {% if filename.endswith('.html') %}html{% elif filename.endswith('.css') %}css{% elif filename.endswith('.js') %}js{% endif %}"></div>
|
||||
<span class="file-name">{{ filename }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Hidden textareas to retain file content (one per file) -->
|
||||
{% for filename, content in files.items() %}
|
||||
<textarea hidden name="{{ filename }}" id="textarea-{{ loop.index0 }}" data-filename="{{ filename }}">{{ content|trim }}</textarea>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Ace Editor Container -->
|
||||
<div id="editor-container" style="width: 100%; height: 400px; border: 1px solid #ddd; margin-top: 20px;"></div>
|
||||
|
||||
<button type="submit" class="btn" style="margin-top:20px;">Save Changes</button>
|
||||
</form>
|
||||
|
||||
<!-- Ace Editor -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ace.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ext-themelist.js"></script>
|
||||
|
||||
<!-- Select2 -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='editor.js') }}"></script>
|
||||
|
||||
<style>
|
||||
/* Style for the Select2 dropdown */
|
||||
.select2-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.main-container {
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
width: 260px;
|
||||
text-align: center;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background: var(--nav-bg);
|
||||
text-align: center;
|
||||
color: var(--nav-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<h1>🚀 ION Static Site Hosting</h1>
|
||||
<p>Host and share your static sites effortlessly.</p>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<h2>Why Choose Us?</h2>
|
||||
<p>Whether you’re a developer, designer, or just need to showcase your work, we make it easy to host and share
|
||||
static sites.</p>
|
||||
|
||||
<div class="feature-list">
|
||||
<div class="feature">
|
||||
<h3>⚡ Instant Hosting</h3>
|
||||
<p>Upload your static files and get a live site in seconds.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🌙 Dark Mode</h3>
|
||||
<p>A sleek, modern interface that's easy on the eyes.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>✏️ Edit in Browser</h3>
|
||||
<p>Make changes directly to your site with our built-in editor.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🔒 Secure & Private</h3>
|
||||
<p>Your data stays yours, no hidden tracking or ads.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>📂 Simple File Management</h3>
|
||||
<p>Easily organize and update your projects.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>🚀 Blazing Fast</h3>
|
||||
<p>Minimal setup, maximum performance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-section">
|
||||
<h2>Get Started Now</h2>
|
||||
<p>No signup required. Upload your first site and go live instantly!</p>
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn">Upload Your Site</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 ION Static Hosting. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
<form method="POST" action="{{ url_for('main.login') }}">
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Register{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Register</h2>
|
||||
<form method="POST" action="{{ url_for('main.register') }}">
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
# upload_handler.py
|
||||
from flask import flash, redirect, url_for, current_app
|
||||
import os
|
||||
import re
|
||||
from .config import Config
|
||||
from .models import Site
|
||||
from . import db
|
||||
|
||||
# Constants
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB per file
|
||||
MAX_TOTAL_STORAGE = 100 * 1024 * 1024 # 100MB per user
|
||||
ALLOWED_EXTENSIONS = {
|
||||
"html",
|
||||
"css",
|
||||
"js",
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"svg",
|
||||
"ico",
|
||||
"webp",
|
||||
"txt",
|
||||
"pdf",
|
||||
}
|
||||
MAX_FILES_PER_UPLOAD = 50
|
||||
|
||||
|
||||
def get_user_storage(user_id):
|
||||
total_size = 0
|
||||
user_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], str(user_id))
|
||||
if os.path.exists(user_dir):
|
||||
for site_dir in os.listdir(user_dir):
|
||||
site_path = os.path.join(user_dir, site_dir)
|
||||
if os.path.isdir(site_path):
|
||||
for root, _, files in os.walk(site_path):
|
||||
for f in files:
|
||||
fp = os.path.join(root, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
||||
|
||||
|
||||
def handle_upload(current_user, site_name, subdomain, files):
|
||||
# Validation checks
|
||||
if not re.match(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$", subdomain):
|
||||
flash("Invalid subdomain format", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if not site_name or not subdomain:
|
||||
flash("Name and subdomain required", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if Site.query.filter_by(subdomain=subdomain).first():
|
||||
flash("Subdomain taken", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if len(files) > MAX_FILES_PER_UPLOAD:
|
||||
flash(f"Max {MAX_FILES_PER_UPLOAD} files per upload", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
for file in files:
|
||||
if "." not in file.filename:
|
||||
flash(f"File {file.filename} has no extension", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
ext = file.filename.rsplit(".", 1)[1].lower()
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
flash(f".{ext} files not allowed", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
total_new_size = 0
|
||||
for file in files:
|
||||
file.stream.seek(0, os.SEEK_END)
|
||||
size = file.stream.tell()
|
||||
file.stream.seek(0)
|
||||
if size > MAX_FILE_SIZE:
|
||||
flash(f"{file.filename} exceeds {MAX_FILE_SIZE//1024//1024}MB", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
total_new_size += size
|
||||
|
||||
current_storage = get_user_storage(current_user.id)
|
||||
if current_storage + total_new_size > MAX_TOTAL_STORAGE:
|
||||
flash(f"Storage limit ({MAX_TOTAL_STORAGE//1024//1024}MB) exceeded", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if not any(f.filename == "index.html" for f in files):
|
||||
flash("Missing index.html", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
# Create site
|
||||
site = Site(user_id=current_user.id, name=site_name, subdomain=subdomain)
|
||||
db.session.add(site)
|
||||
db.session.commit()
|
||||
|
||||
# Save files
|
||||
site_dir = os.path.join(
|
||||
current_app.config["UPLOAD_FOLDER"], str(current_user.id), str(site.id)
|
||||
)
|
||||
os.makedirs(site_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
for file in files:
|
||||
if file.filename:
|
||||
file.save(os.path.join(site_dir, file.filename))
|
||||
except Exception as e:
|
||||
db.session.delete(site)
|
||||
db.session.commit()
|
||||
flash("Upload failed", "error")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
flash("Site created!", "success")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
Reference in New Issue
Block a user