update readme due release on codeberg #2
142
setup.sh
142
setup.sh
|
@ -182,7 +182,8 @@ from flask import (
|
|||
send_file,
|
||||
jsonify,
|
||||
Markup,
|
||||
make_response
|
||||
make_response,
|
||||
abort
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||
|
@ -230,7 +231,7 @@ logging.getLogger('apscheduler').setLevel(logging.DEBUG)
|
|||
from sqlalchemy.engine import Engine
|
||||
import sqlite3
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from functools import wraps
|
||||
|
||||
@event.listens_for(Engine, "connect")
|
||||
def enable_foreign_keys(dbapi_connection, connection_record):
|
||||
|
@ -278,13 +279,20 @@ def translate(key, lang=None, **kwargs):
|
|||
value = translations.get(key) or fallback_translations.get(key) or key
|
||||
return value.format(**kwargs) if isinstance(value, str) else value
|
||||
|
||||
|
||||
|
||||
|
||||
## DEBUG Translations
|
||||
if app.debug:
|
||||
print(f"Loaded translations for 'de': {TRANSLATIONS.get('de', {})}")
|
||||
|
||||
### Admin decorator
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated or not getattr(current_user, 'is_admin', False):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
convention = {
|
||||
|
@ -363,11 +371,12 @@ def _jinja2_filter_datetime(date, fmt='%d.%m.%Y'):
|
|||
|
||||
# DB Models
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users' # Expliziter Tabellenname
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password = db.Column(db.String(256), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
games = db.relationship(
|
||||
'Game',
|
||||
back_populates='owner',
|
||||
|
@ -526,23 +535,32 @@ def login():
|
|||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if not app.config['REGISTRATION_ENABLED']:
|
||||
flash(translate('No new registrations. They are deactivated!'), 'danger')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
abort(403)
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = generate_password_hash(request.form['password'])
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash(translate('Username already exists', session.get('lang', 'en')), 'danger')
|
||||
password = request.form['password']
|
||||
|
||||
existing_user = User.query.filter_by(username=username).first()
|
||||
if existing_user:
|
||||
flash(translate('Username already exists'), 'error')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
new_user = User(username=username, password=password)
|
||||
|
||||
# make the first user admin
|
||||
is_admin = User.query.count() == 0
|
||||
|
||||
new_user = User(
|
||||
username=username,
|
||||
password=generate_password_hash(password),
|
||||
is_admin=is_admin
|
||||
)
|
||||
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
login_user(new_user)
|
||||
flash(translate('Registration successful'), 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/logout')
|
||||
|
@ -946,6 +964,40 @@ def redeem_page(token):
|
|||
expires_timestamp=int(expires_utc.timestamp() * 1000), # Millisekunden
|
||||
platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
|
||||
|
||||
@app.route('/admin/users')
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_users():
|
||||
users = User.query.all()
|
||||
return render_template('admin_users.html', users=users)
|
||||
|
||||
@app.route('/admin/users/delete/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_delete_user(user_id):
|
||||
if current_user.id == user_id:
|
||||
flash(translate('You cannot delete yourself'), 'error')
|
||||
return redirect(url_for('admin_users'))
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(translate('User deleted successfully'), 'success')
|
||||
return redirect(url_for('admin_users'))
|
||||
|
||||
@app.route('/admin/users/reset_password/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_reset_password(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
new_password = secrets.token_urlsafe(8)
|
||||
user.password = generate_password_hash(new_password)
|
||||
db.session.commit()
|
||||
flash(translate('New password for %(username)s: %(password)s',
|
||||
username=user.username, password=new_password), 'info')
|
||||
return redirect(url_for('admin_users'))
|
||||
|
||||
|
||||
|
||||
# Apprise Notifications
|
||||
import apprise
|
||||
|
@ -1059,7 +1111,7 @@ cat <<HTML_END > templates/base.html
|
|||
<!-- Preload Bootstrap CSS for better LCP -->
|
||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
|
||||
<!-- Eigene Styles -->
|
||||
<!-- My Styles -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
{% if games and games[0].steam_appid %}
|
||||
<link rel="preload"
|
||||
|
@ -1089,7 +1141,7 @@ cat <<HTML_END > templates/base.html
|
|||
<img src="{{ url_for('static', filename='logo_small.webp') }}" alt="Logo" width="150" height="116" style="object-fit:contain; border-radius:8px;">
|
||||
<span>Game Key Manager</span>
|
||||
</a>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="d-flex align-items-center gap-3 flex-grow-1">
|
||||
<form class="d-flex" action="{{ url_for('index') }}" method="GET" role="search" aria-label="{{ _('Search games') }}">
|
||||
<label for="searchInput" class="visually-hidden">{{ _('Search') }}</label>
|
||||
<input class="form-control me-2"
|
||||
|
@ -1117,12 +1169,21 @@ cat <<HTML_END > templates/base.html
|
|||
</ul>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="navbar-nav flex-row gap-2 ms-3">
|
||||
{% if current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin_users') }}">
|
||||
⚙️ {{ _('Admin') }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1132,7 +1193,7 @@ cat <<HTML_END > templates/base.html
|
|||
{% if messages %}
|
||||
<div class="flash-container">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible">
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message|safe }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
@ -1833,6 +1894,49 @@ cat <<HTML_END > templates/footer.html
|
|||
</footer>
|
||||
HTML_END
|
||||
|
||||
# Admin interface
|
||||
cat <<HTML_END > templates/admin_users.html
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>{{ _('User Management') }}</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('Username') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ user.username }}
|
||||
{% if user.is_admin %}<span class="badge bg-primary">Admin</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.id != current_user.id %}
|
||||
<form method="POST" action="{{ url_for('admin_delete_user', user_id=user.id) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">{{ _('Delete') }}</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_reset_password', user_id=user.id) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-warning btn-sm">{{ _('Reset Password') }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
HTML_END
|
||||
|
||||
|
||||
# CSS
|
||||
cat <<CSS_END > static/style.css
|
||||
|
|
Loading…
Reference in New Issue