Add admin features, user profile management, and generation capabilities

- Implemented admin dashboard with user management features including role assignment and deletion.
- Added user profile page for updating email and password.
- Created separate routes and templates for text, image, and video generation with appropriate forms.
- Enhanced navigation with dropdown for generation options and loading overlay for better user experience.
- Introduced comprehensive error handling and user feedback through alerts.
- Updated styles for improved UI consistency and responsiveness.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-27 18:48:01 +02:00
parent f2a9f187f2
commit 53d2d2ffef
10 changed files with 1042 additions and 99 deletions
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Admin — AI Allucanget{% endblock %}
{% block content %}
<div class="card">
<h1>Admin Dashboard</h1>
{% if stats %}
<div class="stats-grid">
<div class="stat-box">
<div class="stat-label">Total users</div>
<div class="stat-value">{{ stats.get('total_users', 0) }}</div>
</div>
<div class="stat-box">
<div class="stat-label">Active tokens</div>
<div class="stat-value">{{ stats.get('active_refresh_tokens', 0) }}</div>
</div>
<div class="stat-box">
<div class="stat-label">Admins</div>
<div class="stat-value">{{ stats.get('admin_users', 0) }}</div>
</div>
</div>
{% endif %}
<h2 class="section-title">Users</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td>{{ u.email }}</td>
<td>
<span class="role-badge role-{{ u.role }}">{{ u.role }}</span>
</td>
<td>
<div class="table-actions">
<!-- Role toggle -->
<form method="post" action="{{ url_for('admin_set_role', user_id=u.id) }}">
<input type="hidden" name="role"
value="{{ 'user' if u.role == 'admin' else 'admin' }}">
<button type="submit" class="btn btn-sm">
Make {{ 'user' if u.role == 'admin' else 'admin' }}
</button>
</form>
<!-- Delete -->
{% if u.id != session.get('user_id') %}
<form method="post" 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">Delete</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr><td colspan="3" class="text-muted">No users found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+53 -28
View File
@@ -1,36 +1,61 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AI Allucanget{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="{{ url_for('index') }}" class="brand">AI Allucanget</a>
<div class="nav-links">
{% if session.get('access_token') %}
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}AI Allucanget{% endblock %}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
</head>
<body>
<header>
<nav>
<a href="{{ url_for('index') }}" class="brand">AI Allucanget</a>
<button class="hamburger" aria-label="Open menu">
<span></span><span></span><span></span>
</button>
<div class="nav-links">
{% if session.get('access_token') %}
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('generate') }}">Generate</a>
<div class="nav-dropdown">
<a href="{{ url_for('generate_text') }}">Generate ▾</a>
<div class="nav-dropdown-menu">
<a href="{{ url_for('generate_text') }}">Text</a>
<a href="{{ url_for('generate_image') }}">Image</a>
<a href="{{ url_for('generate_video') }}">Video</a>
</div>
</div>
<a href="{{ url_for('profile') }}">Profile</a>
{% if session.get('user_role') == 'admin' %}
<a href="{{ url_for('admin') }}">Admin</a>
{% endif %}
<a href="{{ url_for('logout') }}">Log out</a>
{% else %}
{% else %}
<a href="{{ url_for('login') }}">Log in</a>
<a href="{{ url_for('register') }}">Register</a>
{% endif %}
</div>
</nav>
</header>
{% endif %}
</div>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<div id="loading-overlay">
<div class="spinner"></div>
<span class="spinner-label">Working…</span>
</div>
{% block content %}{% endblock %}
</main>
</body>
<main>
{% with messages = get_flashed_messages(with_categories=true) %} {% for
category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %} {% endwith %} {% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>
+5 -43
View File
@@ -1,47 +1,9 @@
{% extends "base.html" %}
{% block title %}Generate — AI Allucanget{% endblock %}
{% block content %}
{% extends "base.html" %} {% block title %}Generate — AI Allucanget{% endblock
%} {% block content %}
<div class="card">
<h1>Generate</h1>
<form method="post">
<label for="type">Type</label>
<select id="type" name="type">
<option value="text">Text</option>
<option value="image">Image</option>
<option value="video">Video</option>
</select>
<label for="model">Model</label>
<input id="model" name="model" type="text" required placeholder="e.g. openai/gpt-4o">
<label for="prompt">Prompt</label>
<textarea id="prompt" name="prompt" rows="4" required placeholder="Describe what you want…"></textarea>
<button type="submit">Generate</button>
</form>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
{% if result %}
<div class="result">
{% if result.get('content') %}
<h2>Result</h2>
<pre>{{ result.content }}</pre>
{% elif result.get('images') %}
<h2>Generated image{{ 's' if result.images|length > 1 }}</h2>
{% for img in result.images %}
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
{% endfor %}
{% elif result.get('status') %}
<h2>Video job</h2>
<p>Status: <strong>{{ result.status }}</strong></p>
{% if result.get('video_url') %}
<video src="{{ result.video_url }}" controls class="generated-video"></video>
{% endif %}
{% endif %}
</div>
{% endif %}
<p class="text-muted">
Choose a generation type from the Generate menu above.
</p>
</div>
{% endblock %}
@@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block title %}Image Generation — AI Allucanget{% endblock %}
{% block content %}
<div class="card">
<h1>Image Generation</h1>
<form method="post">
<label for="model">Model</label>
<input id="model" name="model" type="text" required
placeholder="e.g. openai/dall-e-3"
value="{{ request.form.get('model', '') }}">
<label for="prompt">Prompt</label>
<textarea id="prompt" name="prompt" rows="4" required
placeholder="Describe the image you want…">{{ request.form.get('prompt', '') }}</textarea>
<label for="size">Size</label>
<select id="size" name="size">
<option value="1024x1024" {% if request.form.get('size','1024x1024')=='1024x1024' %}selected{% endif %}>1024×1024</option>
<option value="1792x1024" {% if request.form.get('size')=='1792x1024' %}selected{% endif %}>1792×1024 (landscape)</option>
<option value="1024x1792" {% if request.form.get('size')=='1024x1792' %}selected{% endif %}>1024×1792 (portrait)</option>
<option value="512x512" {% if request.form.get('size')=='512x512' %}selected{% endif %}>512×512</option>
</select>
<label for="n">Number of images</label>
<select id="n" name="n">
<option value="1" {% if request.form.get('n','1')=='1' %}selected{% endif %}>1</option>
<option value="2" {% if request.form.get('n')=='2' %}selected{% endif %}>2</option>
<option value="4" {% if request.form.get('n')=='4' %}selected{% endif %}>4</option>
</select>
<button type="submit">Generate image</button>
</form>
{% if error %}
<div class="alert alert-error mt-2">{{ error }}</div>
{% endif %}
{% if result %}
<div class="result">
<h2>Generated image{{ 's' if result.images|length > 1 }}</h2>
{% for img in result.images %}
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
{% if img.revised_prompt %}
<p class="text-muted mt-1" style="font-size:0.8rem;">{{ img.revised_prompt }}</p>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Text Generation — AI Allucanget{% endblock %}
{% block content %}
<div class="card">
<h1>Text Generation</h1>
<form method="post">
<label for="model">Model</label>
<input id="model" name="model" type="text" required
placeholder="e.g. openai/gpt-4o"
value="{{ request.form.get('model', '') }}">
<label for="prompt">Prompt</label>
<textarea id="prompt" name="prompt" rows="5" required
placeholder="Describe what you want…">{{ request.form.get('prompt', '') }}</textarea>
<button type="submit">Generate text</button>
</form>
{% if error %}
<div class="alert alert-error mt-2">{{ error }}</div>
{% endif %}
{% if result %}
<div class="result">
<h2>Result</h2>
<pre>{{ result.content }}</pre>
{% if result.usage %}
<p class="text-muted mt-1" style="font-size:0.8rem;">
Tokens: {{ result.usage.get('total_tokens', '—') }}
</p>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Video Generation — AI Allucanget{% endblock %}
{% block content %}
<div class="card">
<h1>Video Generation</h1>
<div class="tabs-container">
<div class="tabs">
<button class="tab-btn active" data-tab="text-to-video" type="button">Text to video</button>
<button class="tab-btn" data-tab="image-to-video" type="button">Image to video</button>
</div>
<!-- Text-to-video -->
<div class="tab-panel active" id="tab-text-to-video">
<form method="post">
<input type="hidden" name="mode" value="text">
<label for="model-t">Model</label>
<input id="model-t" name="model" type="text" required
placeholder="e.g. openai/sora-2-pro"
value="{{ request.form.get('model', '') if request.form.get('mode','text')=='text' else '' }}">
<label for="prompt-t">Prompt</label>
<textarea id="prompt-t" name="prompt" rows="4" required
placeholder="Describe the video you want…">{{ request.form.get('prompt', '') if request.form.get('mode','text')=='text' else '' }}</textarea>
<label for="aspect-t">Aspect ratio</label>
<select id="aspect-t" name="aspect_ratio">
<option value="16:9">16:9 (landscape)</option>
<option value="9:16">9:16 (portrait)</option>
<option value="1:1">1:1 (square)</option>
</select>
<button type="submit">Generate video</button>
</form>
</div>
<!-- Image-to-video -->
<div class="tab-panel" id="tab-image-to-video">
<form method="post">
<input type="hidden" name="mode" value="image">
<label for="model-i">Model</label>
<input id="model-i" name="model" type="text" required
placeholder="e.g. openai/sora-2-pro"
value="{{ request.form.get('model', '') if request.form.get('mode')=='image' else '' }}">
<label for="image_url">Source image URL</label>
<input id="image_url" name="image_url" type="url" required
placeholder="https://example.com/photo.jpg"
value="{{ request.form.get('image_url', '') }}">
<label for="prompt-i">Motion prompt</label>
<textarea id="prompt-i" name="prompt" rows="3" required
placeholder="Describe the motion or transformation…">{{ request.form.get('prompt', '') if request.form.get('mode')=='image' else '' }}</textarea>
<label for="aspect-i">Aspect ratio</label>
<select id="aspect-i" name="aspect_ratio">
<option value="16:9">16:9 (landscape)</option>
<option value="9:16">9:16 (portrait)</option>
<option value="1:1">1:1 (square)</option>
</select>
<button type="submit">Generate video from image</button>
</form>
</div>
</div>
{% if error %}
<div class="alert alert-error mt-2">{{ error }}</div>
{% endif %}
{% if result %}
<div class="result">
<h2>Video job</h2>
<p>Status: <strong>{{ result.status }}</strong></p>
{% if result.get('video_url') %}
<video src="{{ result.video_url }}" controls class="generated-video"></video>
{% else %}
<p class="text-muted mt-1" style="font-size:0.875rem;">
Video is being processed. Check back later.
</p>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %} {% block title %}Profile — AI Allucanget{% endblock %}
{% block content %}
<div class="card">
<h1>Your Profile</h1>
<h2 class="section-title" style="margin-top: 0">Account details</h2>
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1.5rem">
Current email:
<strong style="color: var(--text)">{{ user.get('email', '') }}</strong>
&nbsp;·&nbsp; Role:
<span class="role-badge role-{{ user.get('role','user') }}"
>{{ user.get('role', 'user') }}</span
>
</p>
<h2 class="section-title">Update email</h2>
<form method="post">
<label for="email">New email</label>
<input
id="email"
name="email"
type="email"
placeholder="{{ user.get('email', '') }}"
/>
<input type="hidden" name="password" value="" />
<button type="submit">Save email</button>
</form>
<h2 class="section-title" style="margin-top: 2rem">Change password</h2>
<form method="post">
<label for="password">New password</label>
<input
id="password"
name="password"
type="password"
placeholder="Enter new password"
minlength="8"
/>
<input type="hidden" name="email" value="" />
<button type="submit">Save password</button>
</form>
</div>
{% endblock %}