Release 1.2: bulk upload, S3/SFTP/FTP, SMTP, password reset, user groups, git deploy
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<nav class="admin-nav">
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="admin-nav__link {% if request.endpoint == 'admin.dashboard' %}admin-nav__link--active{% endif %}">Обзор</a>
|
||||
<a href="{{ url_for('admin.users') }}" class="admin-nav__link {% if request.endpoint == 'admin.users' %}admin-nav__link--active{% endif %}">Пользователи</a>
|
||||
<a href="{{ url_for('admin.groups') }}" class="admin-nav__link {% if request.endpoint in ['admin.groups', 'admin.edit_group', 'admin.delete_group'] %}admin-nav__link--active{% endif %}">Группы</a>
|
||||
<a href="{{ url_for('admin.photos') }}" class="admin-nav__link {% if request.endpoint == 'admin.photos' %}admin-nav__link--active{% endif %}">Фото</a>
|
||||
<a href="{{ url_for('admin.deploy') }}" class="admin-nav__link {% if request.endpoint == 'admin.deploy' %}admin-nav__link--active{% endif %}">Версии Git</a>
|
||||
<a href="{{ url_for('admin.settings') }}" class="admin-nav__link {% if request.endpoint == 'admin.settings' %}admin-nav__link--active{% endif %}">Настройки</a>
|
||||
</nav>
|
||||
|
||||
@@ -29,12 +29,24 @@
|
||||
<span class="stat-card__value">{{ stats.admins }}</span>
|
||||
<span class="stat-card__label">администраторов</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ stats.groups }}</span>
|
||||
<span class="stat-card__label">групп</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ format_size(stats.storage) }}</span>
|
||||
<span class="stat-card__label">хранилище</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_version %}
|
||||
<p class="folder-hint" style="margin-bottom: 24px;">
|
||||
Версия Git: <strong>{{ current_version }}</strong>
|
||||
· <a href="{{ url_for('admin.deploy') }}">Управление версиями</a>
|
||||
{% if not deploy_enabled %}(deploy выключен){% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="admin-grid">
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">Новые пользователи</h2>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Версии Git — Админка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Обновление и версии Git</h1>
|
||||
<p class="page-header__subtitle">Переключение между релизами и пересборка Docker</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ status.current or '—' }}</span>
|
||||
<span class="stat-card__label">текущая версия</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{% if status.repo_ready %}OK{% else %}—{% endif %}</span>
|
||||
<span class="stat-card__label">репозиторий</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{% if status.enabled %}ON{% else %}OFF{% endif %}</span>
|
||||
<span class="stat-card__label">deploy из админки</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not status.repo_ready %}
|
||||
<div class="alert alert--error">
|
||||
Git-репозиторий недоступен по пути <code>{{ status.repo_path }}</code>.
|
||||
Смонтируйте проект в контейнер: <code>./:/repo</code> в docker-compose.yml
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not status.enabled %}
|
||||
<div class="alert alert--error">
|
||||
Обновление через админку отключено. Установите <code>ALLOW_GIT_DEPLOY=true</code> в .env
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="admin-grid">
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">1. Обновить список версий</h2>
|
||||
<p class="folder-hint">Путь: {{ status.repo_path }}{% if status.remote_url %} · {{ status.remote_url }}{% endif %}</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="fetch">
|
||||
<button type="submit" class="btn btn--primary" {% if not status.repo_ready %}disabled{% endif %}>git fetch</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">2. Переключить версию</h2>
|
||||
<form method="post" class="auth-form">
|
||||
<input type="hidden" name="action" value="checkout">
|
||||
<div class="form-group">
|
||||
<label for="ref">Тег или ветка</label>
|
||||
<input type="text" id="ref" name="ref" list="git-refs" required placeholder="v1.1">
|
||||
<datalist id="git-refs">
|
||||
{% for tag in status.tags %}
|
||||
<option value="{{ tag }}">
|
||||
{% endfor %}
|
||||
{% for branch in status.branches %}
|
||||
<option value="{{ branch }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary" {% if not status.repo_ready %}disabled{% endif %}>git checkout</button>
|
||||
</form>
|
||||
{% if status.tags %}
|
||||
<p class="folder-hint">Теги: {{ status.tags[:8]|join(', ') }}{% if status.tags|length > 8 %}…{% endif %}</p>
|
||||
{% elif status.tags_error %}
|
||||
<p class="folder-hint">{{ status.tags_error }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">3. Пересобрать Docker</h2>
|
||||
<p class="folder-hint">Требуется доступ к <code>/var/run/docker.sock</code> в контейнере web</p>
|
||||
<form method="post" onsubmit="return confirm('Пересобрать и перезапустить контейнеры?');">
|
||||
<input type="hidden" name="action" value="rebuild">
|
||||
<button type="submit" class="btn btn--primary" {% if not status.enabled or not status.repo_ready %}disabled{% endif %}>docker compose up -d --build</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros.html" import format_size %}
|
||||
|
||||
{% block title %}Группы — Админка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Группы пользователей</h1>
|
||||
<p class="page-header__subtitle">Квоты дискового пространства и назначение пользователей</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<div class="admin-panel folder-create">
|
||||
<h2 class="admin-panel__title">Создать группу</h2>
|
||||
<form method="post" class="auth-form folder-create__form">
|
||||
<div class="form-group">
|
||||
<label for="name">Название</label>
|
||||
<input type="text" id="name" name="name" required minlength="2" placeholder="VIP">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="disk_quota_mb">Квота (МБ, 0 = без лимита)</label>
|
||||
<input type="number" id="disk_quota_mb" name="disk_quota_mb" min="0" value="500">
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary">Создать группу</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Группа</th>
|
||||
<th>Квота</th>
|
||||
<th>Пользователей</th>
|
||||
<th>Занято</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in group_stats %}
|
||||
{% set group = item.group %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ group.name }}
|
||||
{% if group.is_default %}<span class="badge badge--success">по умолчанию</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ group.quota_label }}</td>
|
||||
<td>{{ group.user_count }}</td>
|
||||
<td>{{ format_size(item.storage_used) }}</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('admin.edit_group', group_id=group.id) }}" class="group-edit-form">
|
||||
<input type="text" name="name" value="{{ group.name }}" required minlength="2" class="form-inline-input">
|
||||
<input type="number" name="disk_quota_mb" value="{{ group.disk_quota_mb }}" min="0" class="form-inline-input form-inline-input--sm">
|
||||
<button type="submit" class="btn btn--ghost btn--sm">Сохранить</button>
|
||||
</form>
|
||||
{% if not group.is_default %}
|
||||
<form method="post" action="{{ url_for('admin.delete_group', group_id=group.id) }}" style="margin-top:6px" onsubmit="return confirm('Удалить группу? Пользователи будут перенесены в группу по умолчанию.');">
|
||||
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Настройки — Админка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Настройки системы</h1>
|
||||
<p class="page-header__subtitle">S3, SFTP, FTP, SMTP и лимиты загрузки</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<form method="post" class="settings-form">
|
||||
<input type="hidden" name="action" value="save">
|
||||
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">Загрузка фото</h2>
|
||||
<div class="form-group">
|
||||
<label for="max_bulk_upload">Максимум файлов за раз (до 100)</label>
|
||||
<input type="number" id="max_bulk_upload" name="max_bulk_upload" min="1" max="100" value="{{ settings.max_bulk_upload }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="margin-top:24px">
|
||||
<h2 class="admin-panel__title">Amazon S3 / совместимое хранилище</h2>
|
||||
<label class="form-checkbox"><input type="checkbox" name="s3_enabled" {% if settings.s3_enabled %}checked{% endif %}><span>Включить S3</span></label>
|
||||
<div class="settings-grid">
|
||||
<div class="form-group"><label>Endpoint</label><input type="text" name="s3_endpoint" value="{{ settings.s3_endpoint or '' }}" placeholder="https://s3.amazonaws.com"></div>
|
||||
<div class="form-group"><label>Bucket</label><input type="text" name="s3_bucket" value="{{ settings.s3_bucket or '' }}"></div>
|
||||
<div class="form-group"><label>Access Key</label><input type="text" name="s3_access_key" value="{{ settings.s3_access_key or '' }}"></div>
|
||||
<div class="form-group"><label>Secret Key</label><input type="password" name="s3_secret_key" placeholder="оставьте пустым, если не меняете"></div>
|
||||
<div class="form-group"><label>Region</label><input type="text" name="s3_region" value="{{ settings.s3_region or 'us-east-1' }}"></div>
|
||||
<div class="form-group"><label>Public URL (CDN)</label><input type="text" name="s3_public_url" value="{{ settings.s3_public_url or '' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="margin-top:24px">
|
||||
<h2 class="admin-panel__title">SFTP (резервная копия)</h2>
|
||||
<label class="form-checkbox"><input type="checkbox" name="sftp_enabled" {% if settings.sftp_enabled %}checked{% endif %}><span>Включить SFTP</span></label>
|
||||
<div class="settings-grid">
|
||||
<div class="form-group"><label>Host</label><input type="text" name="sftp_host" value="{{ settings.sftp_host or '' }}"></div>
|
||||
<div class="form-group"><label>Port</label><input type="number" name="sftp_port" value="{{ settings.sftp_port }}"></div>
|
||||
<div class="form-group"><label>Username</label><input type="text" name="sftp_username" value="{{ settings.sftp_username or '' }}"></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" name="sftp_password" placeholder="оставьте пустым, если не меняете"></div>
|
||||
<div class="form-group"><label>Remote path</label><input type="text" name="sftp_remote_path" value="{{ settings.sftp_remote_path or '/uploads' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="margin-top:24px">
|
||||
<h2 class="admin-panel__title">FTP</h2>
|
||||
<label class="form-checkbox"><input type="checkbox" name="ftp_enabled" {% if settings.ftp_enabled %}checked{% endif %}><span>Включить FTP</span></label>
|
||||
<label class="form-checkbox"><input type="checkbox" name="ftp_use_tls" {% if settings.ftp_use_tls %}checked{% endif %}><span>FTPS (TLS)</span></label>
|
||||
<div class="settings-grid">
|
||||
<div class="form-group"><label>Host</label><input type="text" name="ftp_host" value="{{ settings.ftp_host or '' }}"></div>
|
||||
<div class="form-group"><label>Port</label><input type="number" name="ftp_port" value="{{ settings.ftp_port }}"></div>
|
||||
<div class="form-group"><label>Username</label><input type="text" name="ftp_username" value="{{ settings.ftp_username or '' }}"></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" name="ftp_password" placeholder="оставьте пустым, если не меняете"></div>
|
||||
<div class="form-group"><label>Remote path</label><input type="text" name="ftp_remote_path" value="{{ settings.ftp_remote_path or '/uploads' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="margin-top:24px">
|
||||
<h2 class="admin-panel__title">SMTP (email)</h2>
|
||||
<label class="form-checkbox"><input type="checkbox" name="smtp_enabled" {% if settings.smtp_enabled %}checked{% endif %}><span>Включить SMTP</span></label>
|
||||
<label class="form-checkbox"><input type="checkbox" name="smtp_use_tls" {% if settings.smtp_use_tls %}checked{% endif %}><span>TLS</span></label>
|
||||
<div class="settings-grid">
|
||||
<div class="form-group"><label>Host</label><input type="text" name="smtp_host" value="{{ settings.smtp_host or '' }}" placeholder="smtp.gmail.com"></div>
|
||||
<div class="form-group"><label>Port</label><input type="number" name="smtp_port" value="{{ settings.smtp_port }}"></div>
|
||||
<div class="form-group"><label>Username</label><input type="text" name="smtp_username" value="{{ settings.smtp_username or '' }}"></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" name="smtp_password" placeholder="оставьте пустым, если не меняете"></div>
|
||||
<div class="form-group"><label>From email</label><input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email or '' }}"></div>
|
||||
<div class="form-group"><label>From name</label><input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name or 'PhotoHost' }}"></div>
|
||||
</div>
|
||||
<p class="folder-hint">SMTP используется для сброса пароля, регистрации и уведомлений о загрузке.</p>
|
||||
</div>
|
||||
|
||||
<div class="page-header__actions" style="margin-top:24px">
|
||||
<button type="submit" class="btn btn--primary">Сохранить настройки</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" style="margin-top:16px">
|
||||
<input type="hidden" name="action" value="test_smtp">
|
||||
<button type="submit" class="btn btn--ghost">Отправить тестовое письмо на {{ current_user.email }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -21,6 +21,7 @@
|
||||
<th>ID</th>
|
||||
<th>Логин</th>
|
||||
<th>Email</th>
|
||||
<th>Группа</th>
|
||||
<th>Фото</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
@@ -34,6 +35,15 @@
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('admin.set_user_group', user_id=user.id) }}" method="post" class="group-assign-form">
|
||||
<select name="group_id" class="form-select form-select--sm" onchange="this.form.submit()">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.id }}" {% if user.group_id == group.id %}selected{% endif %}>{{ group.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
<td>{{ user.photo_count }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
|
||||
Reference in New Issue
Block a user