Add blueprints for authentication, admin, dashboard, gallery, generation, and profile routes

- Created `__init__.py` for blueprint registration.
- Implemented `auth.py` for user authentication (login, register, logout).
- Added `admin.py` for admin functionalities (user management, stats).
- Developed `dashboard.py` for user dashboard displaying user info and generated content.
- Created `gallery.py` for managing and displaying images and videos.
- Implemented `generate.py` for text, image, and video generation functionalities.
- Added `profile.py` for user profile management.
- Updated templates to reflect new route structures and improve navigation.
This commit is contained in:
2026-05-30 19:39:30 +02:00
parent 02fc5995db
commit f43b13f625
20 changed files with 667 additions and 617 deletions
+17
View File
@@ -0,0 +1,17 @@
"""Flask frontend application factory."""
from flask import Flask
from .config import Config
from .filters import register_filters
from .routes import register_blueprints
def create_app() -> Flask:
"""Create and configure the Flask application."""
app = Flask(__name__)
app.config.from_object(Config)
register_filters(app)
register_blueprints(app)
return app
+41
View File
@@ -0,0 +1,41 @@
"""Jinja template filters for the frontend app."""
from datetime import datetime, timezone
def from_iso_format(s: str) -> datetime:
"""Convert ISO 8601 string to datetime object."""
return datetime.fromisoformat(s)
def human_time(dt: datetime) -> str:
"""Format a datetime object into a human-readable relative time."""
now = datetime.now(timezone.utc)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
diff = now - dt
seconds = diff.total_seconds()
if seconds < 60:
return "just now"
elif seconds < 3600:
minutes = int(seconds / 60)
return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
elif seconds < 86400:
hours = int(seconds / 3600)
return f"{hours} hour{'s' if hours > 1 else ''} ago"
elif seconds < 2592000:
days = int(seconds / 86400)
return f"{days} day{'s' if days > 1 else ''} ago"
elif seconds < 31536000:
months = int(seconds / 2592000)
return f"{months} month{'s' if months > 1 else ''} ago"
else:
years = int(seconds / 31536000)
return f"{years} year{'s' if years > 1 else ''} ago"
def register_filters(app):
"""Register all template filters on the Flask app."""
app.template_filter("fromisoformat")(from_iso_format)
app.template_filter("humantime")(human_time)
+92
View File
@@ -0,0 +1,92 @@
"""Helper utilities for the frontend app."""
import functools
import httpx
from flask import redirect, session, url_for, flash
def _backend(path: str) -> str:
from flask import current_app
return f"{current_app.config['BACKEND_URL']}{path}"
def _api(method: str, path: str, *, token: str | None = None, **kwargs):
headers = kwargs.pop("headers", {})
if token:
headers["Authorization"] = f"Bearer {token}"
return httpx.request(method, _backend(path), headers=headers, timeout=30, **kwargs)
def _model_matches_modality(model: dict, modality: str) -> bool:
"""Heuristic fallback when backend modality filter returns empty."""
model_modality = (model.get("modality") or "").lower()
if model_modality == modality:
return True
text = f"{model.get('id', '')} {model.get('name', '')}".lower()
keywords = {
"image": ["image", "dall-e", "flux", "stable-diffusion", "sdxl", "recraft", "ideogram", "gpt-image"],
"video": ["video", "sora", "runway", "veo", "kling", "pika", "luma", "wan"],
"audio": ["audio", "speech", "voice", "tts", "transcribe", "whisper"],
}
if modality in keywords:
return any(k in text for k in keywords[modality])
if modality == "text":
non_text_hits = any(
k in text for k in keywords["image"] + keywords["video"] + keywords["audio"])
return not non_text_hits
return False
def _load_models(token: str, modality: str) -> list[dict]:
"""Load models for modality; fallback to unfiltered cache if needed."""
try:
models_resp = _api("GET", "/models/", token=token,
params={"modality": modality})
except httpx.RequestError:
return []
if models_resp.status_code == 200:
try:
models = models_resp.json()
except ValueError:
models = []
if models:
return models
try:
all_resp = _api("GET", "/models/", token=token)
except httpx.RequestError:
return []
if all_resp.status_code != 200:
return []
try:
all_models = all_resp.json()
except ValueError:
return []
filtered = [m for m in all_models if _model_matches_modality(m, modality)]
return filtered or all_models
def login_required(view):
@functools.wraps(view)
def wrapped(*args, **kwargs):
if "access_token" not in session:
return redirect(url_for("auth.login"))
return view(*args, **kwargs)
return wrapped
def admin_required(view):
@functools.wraps(view)
def wrapped(*args, **kwargs):
if "access_token" not in session:
return redirect(url_for("auth.login"))
if session.get("user_role") != "admin":
flash("Admin access required.", "error")
return redirect(url_for("dashboard.index"))
return view(*args, **kwargs)
return wrapped
+5 -585
View File
@@ -1,587 +1,7 @@
"""Flask frontend application."""
import functools
from datetime import datetime, timezone
"""Flask frontend application entry point."""
from . import create_app
import httpx
from flask import (
Flask,
Response,
flash,
jsonify,
redirect,
render_template,
request,
session,
url_for,
)
app = create_app()
from .config import Config
app = Flask(__name__)
app.config.from_object(Config)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@app.template_filter("fromisoformat")
def from_iso_format(s: str) -> datetime:
"""Convert ISO 8601 string to datetime object."""
return datetime.fromisoformat(s)
@app.template_filter("humantime")
def human_time(dt: datetime) -> str:
"""Format a datetime object into a human-readable relative time."""
now = datetime.now(timezone.utc)
# Ensure dt is aware for comparison
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
diff = now - dt
seconds = diff.total_seconds()
if seconds < 60:
return "just now"
elif seconds < 3600:
minutes = int(seconds / 60)
return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
elif seconds < 86400:
hours = int(seconds / 3600)
return f"{hours} hour{'s' if hours > 1 else ''} ago"
elif seconds < 2592000:
days = int(seconds / 86400)
return f"{days} day{'s' if days > 1 else ''} ago"
elif seconds < 31536000:
months = int(seconds / 2592000)
return f"{months} month{'s' if months > 1 else ''} ago"
else:
years = int(seconds / 31536000)
return f"{years} year{'s' if years > 1 else ''} ago"
def _backend(path: str) -> str:
return f"{app.config['BACKEND_URL']}{path}"
def _api(method: str, path: str, *, token: str | None = None, **kwargs):
headers = kwargs.pop("headers", {})
if token:
headers["Authorization"] = f"Bearer {token}"
return httpx.request(method, _backend(path), headers=headers, timeout=30, **kwargs)
def _model_matches_modality(model: dict, modality: str) -> bool:
"""Heuristic fallback when backend modality filter returns empty."""
model_modality = (model.get("modality") or "").lower()
if model_modality == modality:
return True
text = f"{model.get('id', '')} {model.get('name', '')}".lower()
keywords = {
"image": ["image", "dall-e", "flux", "stable-diffusion", "sdxl", "recraft", "ideogram", "gpt-image"],
"video": ["video", "sora", "runway", "veo", "kling", "pika", "luma", "wan"],
"audio": ["audio", "speech", "voice", "tts", "transcribe", "whisper"],
}
if modality in keywords:
return any(k in text for k in keywords[modality])
if modality == "text":
non_text_hits = any(
k in text for k in keywords["image"] + keywords["video"] + keywords["audio"])
return not non_text_hits
return False
def _load_models(token: str, modality: str) -> list[dict]:
"""Load models for modality; fallback to unfiltered cache if needed."""
try:
models_resp = _api("GET", "/models/", token=token,
params={"modality": modality})
except httpx.RequestError:
return []
if models_resp.status_code == 200:
try:
models = models_resp.json()
except ValueError:
models = []
if models:
return models
try:
all_resp = _api("GET", "/models/", token=token)
except httpx.RequestError:
return []
if all_resp.status_code != 200:
return []
try:
all_models = all_resp.json()
except ValueError:
return []
filtered = [m for m in all_models if _model_matches_modality(m, modality)]
return filtered or all_models
def login_required(view):
@functools.wraps(view)
def wrapped(*args, **kwargs):
if "access_token" not in session:
return redirect(url_for("login"))
return view(*args, **kwargs)
return wrapped
def admin_required(view):
@functools.wraps(view)
def wrapped(*args, **kwargs):
if "access_token" not in session:
return redirect(url_for("login"))
if session.get("user_role") != "admin":
flash("Admin access required.", "error")
return redirect(url_for("dashboard"))
return view(*args, **kwargs)
return wrapped
# ---------------------------------------------------------------------------
# Auth routes
# ---------------------------------------------------------------------------
@app.get("/")
def index():
if "access_token" in session:
return redirect(url_for("dashboard"))
return redirect(url_for("login"))
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form["email"]
password = request.form["password"]
resp = _api("POST", "/auth/login",
json={"email": email, "password": password})
if resp.status_code == 200:
data = resp.json()
session["access_token"] = data["access_token"]
session["refresh_token"] = data["refresh_token"]
me = _api("GET", "/users/me", token=data["access_token"])
if me.status_code == 200:
u = me.json()
session["user_email"] = u.get("email", "")
session["user_role"] = u.get("role", "user")
return redirect(url_for("dashboard"))
flash("Invalid email or password.", "error")
return render_template("login.html")
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
email = request.form["email"]
password = request.form["password"]
resp = _api("POST", "/auth/register",
json={"email": email, "password": password})
if resp.status_code == 201:
flash("Account created. Please log in.", "success")
return redirect(url_for("login"))
detail = resp.json().get("detail", "Registration failed.")
flash(detail, "error")
return render_template("register.html")
@app.get("/logout")
def logout():
refresh_token = session.get("refresh_token")
if refresh_token:
_api("POST", "/auth/logout", json={"refresh_token": refresh_token})
session.clear()
return redirect(url_for("login"))
# ---------------------------------------------------------------------------
# Authenticated routes
# ---------------------------------------------------------------------------
@app.get("/dashboard")
@login_required
def dashboard():
token = session["access_token"]
resp = _api("GET", "/users/me", token=token)
user = resp.json() if resp.status_code == 200 else {}
img_resp = _api("GET", "/images/", token=token)
images = img_resp.json() if img_resp.status_code == 200 else []
gen_resp = _api("GET", "/generate/images", token=token)
generated_images = gen_resp.json() if gen_resp.status_code == 200 else []
vid_resp = _api("GET", "/generate/videos", token=token)
videos = vid_resp.json() if vid_resp.status_code == 200 else []
pending_videos = [v for v in videos if v.get(
"status") not in ("completed", "failed")]
completed_videos = [v for v in videos if v.get("status") == "completed"]
return render_template("dashboard.html", user=user, images=images,
generated_images=generated_images,
pending_videos=pending_videos,
completed_videos=completed_videos)
@app.get("/gallery")
@login_required
def gallery():
token = session["access_token"]
# Fetch all content types
uploads_resp = _api("GET", "/images/", token=token)
uploads = uploads_resp.json() if uploads_resp.status_code == 200 else []
gen_images_resp = _api("GET", "/generate/images", token=token)
generated_images = gen_images_resp.json(
) if gen_images_resp.status_code == 200 else []
videos_resp = _api("GET", "/generate/videos", token=token)
videos = videos_resp.json() if videos_resp.status_code == 200 else []
# Separate pending videos
pending_videos = [v for v in videos if v.get(
"status") not in ("completed", "failed")]
completed_videos = [v for v in videos if v.get("status") == "completed"]
return render_template(
"gallery.html",
uploads=uploads,
generated_images=generated_images,
pending_videos=pending_videos,
completed_videos=completed_videos,
)
@app.get("/gallery/image/<image_id>")
@login_required
def image_detail(image_id: str):
token = session["access_token"]
resp = _api("GET", f"/generate/images/{image_id}", token=token)
image = resp.json() if resp.status_code == 200 else None
return render_template("image_detail.html", image=image)
@app.get("/gallery/video/<video_id>")
@login_required
def video_detail(video_id: str):
token = session["access_token"]
resp = _api("GET", f"/generate/videos/{video_id}", token=token)
video = resp.json() if resp.status_code == 200 else None
return render_template("video_detail.html", video=video)
@app.get("/gallery/upload/<image_id>")
@login_required
def upload_detail(image_id: str):
token = session["access_token"]
resp = _api("GET", f"/images/{image_id}", token=token)
image = resp.json() if resp.status_code == 200 else None
return render_template("upload_detail.html", image=image)
# ── Generate ──────────────────────────────────────────────────────────────
@app.get("/images/<image_id>/file")
@login_required
def serve_uploaded_image(image_id: str):
resp = _api("GET", f"/images/{image_id}/file",
token=session["access_token"])
if resp.status_code != 200:
return Response("Not found", status=404)
return Response(
resp.content,
status=200,
content_type=resp.headers.get("content-type", "image/jpeg"),
)
@app.get("/generate")
@login_required
def generate():
return redirect(url_for("generate_text"))
@app.route("/generate/text", methods=["GET", "POST"])
@login_required
def generate_text():
error = None
token = session["access_token"]
chat_history: list[dict] = session.get("chat_history", [])
system_prompt: str = session.get("chat_system_prompt", "")
model: str = session.get("chat_model", "")
if request.method == "POST":
action = request.form.get("action", "send")
if action == "clear":
session.pop("chat_history", None)
session.pop("chat_system_prompt", None)
session.pop("chat_model", None)
return redirect(url_for("generate_text"))
prompt = request.form.get("prompt", "").strip()
model = request.form.get("model", "").strip()
system_prompt = request.form.get("system_prompt", "").strip()
# Persist model + system_prompt across turns
session["chat_model"] = model
session["chat_system_prompt"] = system_prompt
if prompt:
# Build messages: history (user/assistant only) + new user msg
messages = [m for m in chat_history if m["role"]
in ("user", "assistant")]
messages.append({"role": "user", "content": prompt})
payload: dict = {
"model": model,
"messages": [{"role": m["role"], "content": m["content"]} for m in messages],
}
if system_prompt:
payload["system_prompt"] = system_prompt
resp = _api("POST", "/generate/text", token=token, json=payload)
if resp.status_code == 200:
data = resp.json()
chat_history = list(messages)
chat_history.append({"role": "assistant", "content": data["content"],
"usage": data.get("usage")})
session["chat_history"] = chat_history
else:
try:
error = resp.json().get("detail", "Generation failed.")
except Exception:
error = "Generation failed."
models = _load_models(token, "text")
return render_template(
"generate_text.html",
chat_history=session.get("chat_history", []),
error=error,
models=models,
system_prompt=system_prompt,
current_model=model,
)
@app.route("/generate/image", methods=["GET", "POST"])
@login_required
def generate_image():
result = error = None
token = session["access_token"]
if request.method == "POST":
# Upload reference image if provided
ref_file = request.files.get("reference_image")
if ref_file and ref_file.filename:
up_resp = _api(
"POST", "/images/upload",
token=token,
files={"file": (ref_file.filename,
ref_file.stream, ref_file.content_type)},
)
if up_resp.status_code not in (200, 201):
error = up_resp.json().get("detail", "Image upload failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
resp = _api("POST", "/generate/image", token=token, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"n": int(request.form.get("n", 1)),
"size": request.form.get("size", "1024x1024"),
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
"image_size": request.form.get("image_size", "").strip() or None,
})
if resp.status_code == 200:
result = resp.json()
else:
error = resp.json().get("detail", "Generation failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
@app.route("/generate/video", methods=["GET", "POST"])
@login_required
def generate_video():
error = None
token = session["access_token"]
if request.method == "POST":
mode = request.form.get("mode", "text")
duration_raw = request.form.get("duration_seconds", "")
duration = int(
duration_raw) if duration_raw.strip().isdigit() else None
resolution = request.form.get("resolution", "").strip() or None
if mode == "image":
resp = _api("POST", "/generate/video/from-image", token=token, json={
"model": request.form.get("model", "").strip(),
"image_url": request.form.get("image_url", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
"duration_seconds": duration,
"resolution": resolution,
})
else:
resp = _api("POST", "/generate/video", token=token, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
"duration_seconds": duration,
"resolution": resolution,
})
if resp.status_code == 200:
result = resp.json()
# On success, redirect to the detail page to monitor progress
db_id = result.get("db_id")
if db_id:
return redirect(url_for("video_detail", video_id=db_id))
# Fallback for older backend versions
flash("Video job started.", "success")
return redirect(url_for("gallery"))
else:
error = resp.json().get("detail", "Generation failed.")
models = _load_models(token, "video")
return render_template("generate_video.html", error=error, models=models)
@app.get("/generate/video/status")
@login_required
def generate_video_status():
"""Proxy video status polling to the backend."""
polling_url = request.args.get("polling_url", "")
if not polling_url:
return jsonify({"error": "polling_url required"}), 400
resp = _api(
"GET", "/generate/video/status",
token=session["access_token"],
params={"polling_url": polling_url},
)
return jsonify(resp.json()), resp.status_code
@app.get("/generate/video/<video_id>/status")
@login_required
def generate_video_db_status(video_id: str):
"""Return current DB status for a video job (polled by frontend JS)."""
resp = _api(
"GET", f"/generate/videos/{video_id}", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@app.post("/generate/video/<video_id>/cancel")
@login_required
def cancel_video_job(video_id: str):
"""Proxy cancel request to backend."""
resp = _api(
"POST", f"/generate/videos/{video_id}/cancel", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
# ── Admin ─────────────────────────────────────────────────────────────────
@app.get("/admin")
@admin_required
def admin():
token = session["access_token"]
stats_resp = _api("GET", "/admin/stats", token=token)
users_resp = _api("GET", "/users", token=token)
stats = stats_resp.json() if stats_resp.status_code == 200 else {}
users = users_resp.json() if users_resp.status_code == 200 else []
return render_template("admin.html", stats=stats, users=users)
@app.post("/admin/users/<user_id>/role")
@admin_required
def admin_set_role(user_id: str):
role = request.form.get("role", "user")
_api("PUT", f"/users/{user_id}/role",
token=session["access_token"], json={"role": role})
flash(f"Role updated to '{role}'.", "success")
return redirect(url_for("admin"))
@app.post("/admin/users/<user_id>/delete")
@admin_required
def admin_delete_user(user_id: str):
_api("DELETE", f"/users/{user_id}", token=session["access_token"])
flash("User deleted.", "success")
return redirect(url_for("admin"))
@app.get("/admin/models")
@admin_required
def admin_models():
"""Show model cache status and list all models."""
return render_template("admin/models.html")
# ── Admin API proxies (same-origin for browser JS, avoids mixed-content) ──
@app.get("/api/admin/videos")
@admin_required
def api_admin_list_videos():
resp = _api("GET", "/admin/videos", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@app.post("/api/admin/videos/<job_id>/retry")
@admin_required
def api_admin_retry_video(job_id: str):
resp = _api(
"POST", f"/admin/videos/{job_id}/retry", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@app.post("/api/admin/videos/<job_id>/cancel")
@admin_required
def api_admin_cancel_video(job_id: str):
resp = _api(
"POST", f"/admin/videos/{job_id}/cancel", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@app.delete("/api/admin/videos/<job_id>")
@admin_required
def api_admin_delete_video(job_id: str):
resp = _api(
"DELETE", f"/admin/videos/{job_id}", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
# ── Profile ───────────────────────────────────────────────────────────────
@app.route("/users/profile", methods=["GET", "POST"])
@login_required
def profile():
token = session["access_token"]
if request.method == "POST":
payload: dict = {}
new_email = request.form.get("email", "").strip()
new_password = request.form.get("password", "").strip()
if new_email:
payload["email"] = new_email
if new_password:
payload["password"] = new_password
if payload:
resp = _api("PUT", "/users/me", token=token, json=payload)
if resp.status_code == 200:
updated = resp.json()
session["user_email"] = updated.get(
"email", session.get("user_email", ""))
flash("Profile updated.", "success")
else:
flash(resp.json().get("detail", "Update failed."), "error")
return redirect(url_for("profile"))
resp = _api("GET", "/users/me", token=token)
user = resp.json() if resp.status_code == 200 else {}
return render_template("profile.html", user=user)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=12016, debug=True)
+19
View File
@@ -0,0 +1,19 @@
"""Blueprint registration."""
from flask import Flask
def register_blueprints(app: Flask):
"""Register all application blueprints."""
from .auth import auth_bp
from .dashboard import dashboard_bp
from .gallery import gallery_bp
from .generate import generate_bp
from .admin import admin_bp
from .profile import profile_bp
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(gallery_bp)
app.register_blueprint(generate_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(profile_bp)
+75
View File
@@ -0,0 +1,75 @@
"""Admin blueprint."""
from flask import Blueprint, flash, jsonify, redirect, render_template, request, session, url_for
from ..helpers import _api, admin_required
admin_bp = Blueprint("admin", __name__)
@admin_bp.get("/admin")
@admin_required
def index():
token = session["access_token"]
stats_resp = _api("GET", "/admin/stats", token=token)
users_resp = _api("GET", "/users", token=token)
stats = stats_resp.json() if stats_resp.status_code == 200 else {}
users = users_resp.json() if users_resp.status_code == 200 else []
return render_template("admin.html", stats=stats, users=users)
@admin_bp.post("/admin/users/<user_id>/role")
@admin_required
def set_role(user_id: str):
role = request.form.get("role", "user")
_api("PUT", f"/users/{user_id}/role",
token=session["access_token"], json={"role": role})
flash(f"Role updated to '{role}'.", "success")
return redirect(url_for("admin.index"))
@admin_bp.post("/admin/users/<user_id>/delete")
@admin_required
def delete_user(user_id: str):
_api("DELETE", f"/users/{user_id}", token=session["access_token"])
flash("User deleted.", "success")
return redirect(url_for("admin.index"))
@admin_bp.get("/admin/models")
@admin_required
def models():
"""Show model cache status and list all models."""
return render_template("admin/models.html")
# ── Admin API proxies (same-origin for browser JS) ────────────────────────
@admin_bp.get("/api/admin/videos")
@admin_required
def list_videos():
resp = _api("GET", "/admin/videos", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@admin_bp.post("/api/admin/videos/<job_id>/retry")
@admin_required
def retry_video(job_id: str):
resp = _api(
"POST", f"/admin/videos/{job_id}/retry", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@admin_bp.post("/api/admin/videos/<job_id>/cancel")
@admin_required
def cancel_video(job_id: str):
resp = _api(
"POST", f"/admin/videos/{job_id}/cancel", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@admin_bp.delete("/api/admin/videos/<job_id>")
@admin_required
def delete_video(job_id: str):
resp = _api(
"DELETE", f"/admin/videos/{job_id}", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
+58
View File
@@ -0,0 +1,58 @@
"""Auth blueprint — login, register, logout, index."""
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from ..helpers import _api
auth_bp = Blueprint("auth", __name__)
@auth_bp.get("/")
def index():
if "access_token" in session:
return redirect(url_for("dashboard.index"))
return redirect(url_for("auth.login"))
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form["email"]
password = request.form["password"]
resp = _api("POST", "/auth/login",
json={"email": email, "password": password})
if resp.status_code == 200:
data = resp.json()
session["access_token"] = data["access_token"]
session["refresh_token"] = data["refresh_token"]
me = _api("GET", "/users/me", token=data["access_token"])
if me.status_code == 200:
u = me.json()
session["user_email"] = u.get("email", "")
session["user_role"] = u.get("role", "user")
return redirect(url_for("dashboard.index"))
flash("Invalid email or password.", "error")
return render_template("login.html")
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
email = request.form["email"]
password = request.form["password"]
resp = _api("POST", "/auth/register",
json={"email": email, "password": password})
if resp.status_code == 201:
flash("Account created. Please log in.", "success")
return redirect(url_for("auth.login"))
detail = resp.json().get("detail", "Registration failed.")
flash(detail, "error")
return render_template("register.html")
@auth_bp.get("/logout")
def logout():
refresh_token = session.get("refresh_token")
if refresh_token:
_api("POST", "/auth/logout", json={"refresh_token": refresh_token})
session.clear()
return redirect(url_for("auth.login"))
+29
View File
@@ -0,0 +1,29 @@
"""Dashboard blueprint."""
from flask import Blueprint, render_template, session
from ..helpers import _api, login_required
dashboard_bp = Blueprint("dashboard", __name__)
@dashboard_bp.get("/dashboard")
@login_required
def index():
token = session["access_token"]
resp = _api("GET", "/users/me", token=token)
user = resp.json() if resp.status_code == 200 else {}
img_resp = _api("GET", "/images/", token=token)
images = img_resp.json() if img_resp.status_code == 200 else []
gen_resp = _api("GET", "/generate/images", token=token)
generated_images = gen_resp.json() if gen_resp.status_code == 200 else []
vid_resp = _api("GET", "/generate/videos", token=token)
videos = vid_resp.json() if vid_resp.status_code == 200 else []
pending_videos = [v for v in videos if v.get(
"status") not in ("completed", "failed")]
completed_videos = [v for v in videos if v.get("status") == "completed"]
return render_template("dashboard.html", user=user, images=images,
generated_images=generated_images,
pending_videos=pending_videos,
completed_videos=completed_videos)
+75
View File
@@ -0,0 +1,75 @@
"""Gallery blueprint."""
from flask import Blueprint, Response, render_template, session
from ..helpers import _api, login_required
gallery_bp = Blueprint("gallery", __name__)
@gallery_bp.get("/gallery")
@login_required
def index():
token = session["access_token"]
uploads_resp = _api("GET", "/images/", token=token)
uploads = uploads_resp.json() if uploads_resp.status_code == 200 else []
gen_images_resp = _api("GET", "/generate/images", token=token)
generated_images = gen_images_resp.json(
) if gen_images_resp.status_code == 200 else []
videos_resp = _api("GET", "/generate/videos", token=token)
videos = videos_resp.json() if videos_resp.status_code == 200 else []
pending_videos = [v for v in videos if v.get(
"status") not in ("completed", "failed")]
completed_videos = [v for v in videos if v.get("status") == "completed"]
return render_template(
"gallery.html",
uploads=uploads,
generated_images=generated_images,
pending_videos=pending_videos,
completed_videos=completed_videos,
)
@gallery_bp.get("/gallery/image/<image_id>")
@login_required
def image_detail(image_id: str):
token = session["access_token"]
resp = _api("GET", f"/generate/images/{image_id}", token=token)
image = resp.json() if resp.status_code == 200 else None
return render_template("image_detail.html", image=image)
@gallery_bp.get("/gallery/video/<video_id>")
@login_required
def video_detail(video_id: str):
token = session["access_token"]
resp = _api("GET", f"/generate/videos/{video_id}", token=token)
video = resp.json() if resp.status_code == 200 else None
return render_template("video_detail.html", video=video)
@gallery_bp.get("/gallery/upload/<image_id>")
@login_required
def upload_detail(image_id: str):
token = session["access_token"]
resp = _api("GET", f"/images/{image_id}", token=token)
image = resp.json() if resp.status_code == 200 else None
return render_template("upload_detail.html", image=image)
@gallery_bp.get("/images/<image_id>/file")
@login_required
def serve_uploaded_image(image_id: str):
resp = _api("GET", f"/images/{image_id}/file",
token=session["access_token"])
if resp.status_code != 200:
return Response("Not found", status=404)
return Response(
resp.content,
status=200,
content_type=resp.headers.get("content-type", "image/jpeg"),
)
+187
View File
@@ -0,0 +1,187 @@
"""Generate blueprint — text, image, video generation."""
from flask import (
Blueprint, flash, jsonify, redirect, render_template, request, session, url_for,
)
from ..helpers import _api, _load_models, login_required
generate_bp = Blueprint("generate", __name__)
@generate_bp.get("/generate")
@login_required
def index():
return redirect(url_for("generate.text"))
@generate_bp.route("/generate/text", methods=["GET", "POST"])
@login_required
def text():
error = None
token = session["access_token"]
chat_history: list[dict] = session.get("chat_history", [])
system_prompt: str = session.get("chat_system_prompt", "")
model: str = session.get("chat_model", "")
if request.method == "POST":
action = request.form.get("action", "send")
if action == "clear":
session.pop("chat_history", None)
session.pop("chat_system_prompt", None)
session.pop("chat_model", None)
return redirect(url_for("generate.text"))
prompt = request.form.get("prompt", "").strip()
model = request.form.get("model", "").strip()
system_prompt = request.form.get("system_prompt", "").strip()
session["chat_model"] = model
session["chat_system_prompt"] = system_prompt
if prompt:
messages = [m for m in chat_history if m["role"]
in ("user", "assistant")]
messages.append({"role": "user", "content": prompt})
payload: dict = {
"model": model,
"messages": [{"role": m["role"], "content": m["content"]} for m in messages],
}
if system_prompt:
payload["system_prompt"] = system_prompt
resp = _api("POST", "/generate/text", token=token, json=payload)
if resp.status_code == 200:
data = resp.json()
chat_history = list(messages)
chat_history.append({"role": "assistant", "content": data["content"],
"usage": data.get("usage")})
session["chat_history"] = chat_history
else:
try:
error = resp.json().get("detail", "Generation failed.")
except Exception:
error = "Generation failed."
models = _load_models(token, "text")
return render_template(
"generate_text.html",
chat_history=session.get("chat_history", []),
error=error,
models=models,
system_prompt=system_prompt,
current_model=model,
)
@generate_bp.route("/generate/image", methods=["GET", "POST"])
@login_required
def image():
result = error = None
token = session["access_token"]
if request.method == "POST":
ref_file = request.files.get("reference_image")
if ref_file and ref_file.filename:
up_resp = _api(
"POST", "/images/upload",
token=token,
files={"file": (ref_file.filename,
ref_file.stream, ref_file.content_type)},
)
if up_resp.status_code not in (200, 201):
error = up_resp.json().get("detail", "Image upload failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
resp = _api("POST", "/generate/image", token=token, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"n": int(request.form.get("n", 1)),
"size": request.form.get("size", "1024x1024"),
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
"image_size": request.form.get("image_size", "").strip() or None,
})
if resp.status_code == 200:
result = resp.json()
else:
error = resp.json().get("detail", "Generation failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
@generate_bp.route("/generate/video", methods=["GET", "POST"])
@login_required
def video():
error = None
token = session["access_token"]
if request.method == "POST":
mode = request.form.get("mode", "text")
duration_raw = request.form.get("duration_seconds", "")
duration = int(
duration_raw) if duration_raw.strip().isdigit() else None
resolution = request.form.get("resolution", "").strip() or None
if mode == "image":
resp = _api("POST", "/generate/video/from-image", token=token, json={
"model": request.form.get("model", "").strip(),
"image_url": request.form.get("image_url", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
"duration_seconds": duration,
"resolution": resolution,
})
else:
resp = _api("POST", "/generate/video", token=token, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
"duration_seconds": duration,
"resolution": resolution,
})
if resp.status_code == 200:
result = resp.json()
db_id = result.get("db_id")
if db_id:
return redirect(url_for("gallery.video_detail", video_id=db_id))
flash("Video job started.", "success")
return redirect(url_for("gallery.index"))
else:
error = resp.json().get("detail", "Generation failed.")
models = _load_models(token, "video")
return render_template("generate_video.html", error=error, models=models)
@generate_bp.get("/generate/video/status")
@login_required
def video_status():
"""Proxy video status polling to the backend."""
polling_url = request.args.get("polling_url", "")
if not polling_url:
return jsonify({"error": "polling_url required"}), 400
resp = _api(
"GET", "/generate/video/status",
token=session["access_token"],
params={"polling_url": polling_url},
)
return jsonify(resp.json()), resp.status_code
@generate_bp.get("/generate/video/<video_id>/status")
@login_required
def video_db_status(video_id: str):
"""Return current DB status for a video job (polled by frontend JS)."""
resp = _api(
"GET", f"/generate/videos/{video_id}", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
@generate_bp.post("/generate/video/<video_id>/cancel")
@login_required
def cancel_video_job(video_id: str):
"""Proxy cancel request to backend."""
resp = _api(
"POST", f"/generate/videos/{video_id}/cancel", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
+33
View File
@@ -0,0 +1,33 @@
"""Profile blueprint."""
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from ..helpers import _api, login_required
profile_bp = Blueprint("profile", __name__)
@profile_bp.route("/users/profile", methods=["GET", "POST"])
@login_required
def index():
token = session["access_token"]
if request.method == "POST":
payload: dict = {}
new_email = request.form.get("email", "").strip()
new_password = request.form.get("password", "").strip()
if new_email:
payload["email"] = new_email
if new_password:
payload["password"] = new_password
if payload:
resp = _api("PUT", "/users/me", token=token, json=payload)
if resp.status_code == 200:
updated = resp.json()
session["user_email"] = updated.get(
"email", session.get("user_email", ""))
flash("Profile updated.", "success")
else:
flash(resp.json().get("detail", "Update failed."), "error")
return redirect(url_for("profile.index"))
resp = _api("GET", "/users/me", token=token)
user = resp.json() if resp.status_code == 200 else {}
return render_template("profile.html", user=user)
+2 -2
View File
@@ -42,7 +42,7 @@
<!-- Role toggle -->
<form
method="post"
action="{{ url_for('admin_set_role', user_id=u.id) }}"
action="{{ url_for('admin.set_role', user_id=u.id) }}"
>
<input
type="hidden"
@@ -57,7 +57,7 @@
{% if u.id != session.get('user_id') %}
<form
method="post"
action="{{ url_for('admin_delete_user', user_id=u.id) }}"
action="{{ url_for('admin.delete_user', user_id=u.id) }}"
onsubmit="return confirm('Delete {{ u.email }}?')"
>
<button type="submit" class="btn btn-sm btn-danger">
+13 -11
View File
@@ -13,7 +13,9 @@
<body>
<header>
<nav>
<a href="{{ url_for('index') }}" class="brand">All You Can GET AI</a>
<a href="{{ url_for('auth.index') }}" class="brand"
>All You Can GET AI</a
>
<button class="hamburger" aria-label="Open menu">
<span></span><span></span><span></span>
@@ -21,21 +23,21 @@
<div class="nav-links">
{% if session.get('access_token') %}
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('gallery') }}">Gallery</a>
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
<a href="{{ url_for('gallery.index') }}">Gallery</a>
<a href="{{ url_for('generate_text') }}">Generate Text</a>
<a href="{{ url_for('generate_image') }}">Generate Image</a>
<a href="{{ url_for('generate_video') }}">Generate Video</a>
<a href="{{ url_for('generate.text') }}">Generate Text</a>
<a href="{{ url_for('generate.image') }}">Generate Image</a>
<a href="{{ url_for('generate.video') }}">Generate Video</a>
<a href="{{ url_for('profile') }}">Profile</a>
<a href="{{ url_for('profile.index') }}">Profile</a>
{% if session.get('user_role') == 'admin' %}
<a href="{{ url_for('admin') }}">Admin</a>
<a href="{{ url_for('admin.index') }}">Admin</a>
{% endif %}
<a href="{{ url_for('logout') }}">Log out</a>
<a href="{{ url_for('auth.logout') }}">Log out</a>
{% else %}
<a href="{{ url_for('login') }}">Log in</a>
<a href="{{ url_for('register') }}">Register</a>
<a href="{{ url_for('auth.login') }}">Log in</a>
<a href="{{ url_for('auth.register') }}">Register</a>
{% endif %}
</div>
</nav>
+6 -6
View File
@@ -3,7 +3,7 @@ endblock %} {% block content %}
<div class="card">
<h1>Welcome{% if user.get('email') %}, {{ user.email }}{% endif %}</h1>
<p>Role: <strong>{{ user.get('role', 'user') }}</strong></p>
<a href="{{ url_for('generate') }}" class="btn">Start generating</a>
<a href="{{ url_for('generate.index') }}" class="btn">Start generating</a>
</div>
{% if pending_videos %}
@@ -12,7 +12,7 @@ endblock %} {% block content %}
<div class="image-grid">
{% for vid in pending_videos %}
<a
href="{{ url_for('video_detail', video_id=vid.id) }}"
href="{{ url_for('gallery.video_detail', video_id=vid.id) }}"
class="image-grid-item"
>
<div
@@ -39,7 +39,7 @@ endblock %} {% block content %}
<div class="image-grid">
{% for img in generated_images %}
<a
href="{{ url_for('image_detail', image_id=img.id) }}"
href="{{ url_for('gallery.image_detail', image_id=img.id) }}"
class="image-grid-item"
>
<img
@@ -62,7 +62,7 @@ endblock %} {% block content %}
<div class="image-grid">
{% for vid in completed_videos %}
<a
href="{{ url_for('video_detail', video_id=vid.id) }}"
href="{{ url_for('gallery.video_detail', video_id=vid.id) }}"
class="image-grid-item"
>
{% if vid.video_url %}
@@ -97,11 +97,11 @@ endblock %} {% block content %}
<div class="image-grid">
{% for img in images %}
<a
href="{{ url_for('upload_detail', image_id=img.id) }}"
href="{{ url_for('gallery.upload_detail', image_id=img.id) }}"
class="image-grid-item"
>
<img
src="{{ url_for('serve_uploaded_image', image_id=img.id) }}"
src="{{ url_for('gallery.serve_uploaded_image', image_id=img.id) }}"
alt="{{ img.filename }}"
class="generated-image"
loading="lazy"
+7 -7
View File
@@ -22,7 +22,7 @@ content %}
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300 relative"
data-pending-video-id="{{ video.id }}"
>
<a href="{{ url_for('video_detail', video_id=video.id) }}">
<a href="{{ url_for('gallery.video_detail', video_id=video.id) }}">
<div class="p-4">
<p class="font-bold text-lg truncate">{{ video.prompt }}</p>
<p class="text-sm text-gray-400">
@@ -62,7 +62,7 @@ content %}
>
{% for image in generated_images %}
<a
href="{{ url_for('image_detail', image_id=image.id) }}"
href="{{ url_for('gallery.image_detail', image_id=image.id) }}"
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
>
<img
@@ -86,7 +86,7 @@ content %}
<p class="text-gray-400">
You haven't generated any images yet.
<a
href="{{ url_for('generate_image') }}"
href="{{ url_for('generate.image') }}"
class="text-blue-400 hover:underline"
>Generate one now</a
>.
@@ -105,7 +105,7 @@ content %}
>
{% for video in completed_videos %}
<a
href="{{ url_for('video_detail', video_id=video.id) }}"
href="{{ url_for('gallery.video_detail', video_id=video.id) }}"
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
>
{% if video.video_url %}
@@ -154,7 +154,7 @@ content %}
<p class="text-gray-400">
You haven't generated any videos yet.
<a
href="{{ url_for('generate_video') }}"
href="{{ url_for('generate.video') }}"
class="text-blue-400 hover:underline"
>Generate one now</a
>.
@@ -173,11 +173,11 @@ content %}
>
{% for image in uploads %}
<a
href="{{ url_for('upload_detail', image_id=image.id) }}"
href="{{ url_for('gallery.upload_detail', image_id=image.id) }}"
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
>
<img
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
src="{{ url_for('gallery.serve_uploaded_image', image_id=image.id) }}"
alt="{{ image.filename }}"
class="w-full h-48 object-cover"
/>
+1 -1
View File
@@ -2,7 +2,7 @@
block content %}
<div class="container mx-auto px-4 py-8">
<a
href="{{ url_for('gallery') }}"
href="{{ url_for('gallery.index') }}"
class="text-blue-400 hover:underline mb-4 inline-block"
>&larr; Back to Gallery</a
>
+1 -1
View File
@@ -11,6 +11,6 @@ endblock %} {% block content %}
<button type="submit">Log in</button>
</form>
<p>No account? <a href="{{ url_for('register') }}">Register</a></p>
<p>No account? <a href="{{ url_for('auth.register') }}">Register</a></p>
</div>
{% endblock %}
+3 -1
View File
@@ -17,6 +17,8 @@ endblock %} {% block content %}
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="{{ url_for('login') }}">Log in</a></p>
<p>
Already have an account? <a href="{{ url_for('auth.login') }}">Log in</a>
</p>
</div>
{% endblock %}
+2 -2
View File
@@ -2,7 +2,7 @@
content %}
<div class="container mx-auto px-4 py-8">
<a
href="{{ url_for('gallery') }}"
href="{{ url_for('gallery.index') }}"
class="text-blue-400 hover:underline mb-4 inline-block"
>&larr; Back to Gallery</a
>
@@ -11,7 +11,7 @@ content %}
<h1 class="text-2xl font-bold mb-4">Uploaded Image</h1>
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<img
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
src="{{ url_for('gallery.serve_uploaded_image', image_id=image.id) }}"
alt="{{ image.filename }}"
class="w-full object-contain"
/>
+1 -1
View File
@@ -2,7 +2,7 @@
block content %}
<div class="container mx-auto px-4 py-8">
<a
href="{{ url_for('gallery') }}"
href="{{ url_for('gallery.index') }}"
class="text-blue-400 hover:underline mb-4 inline-block"
>&larr; Back to Gallery</a
>