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:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
@@ -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)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
>← Back to Gallery</a
|
||||
>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,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"
|
||||
>← 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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
>← Back to Gallery</a
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user