update readme due release on codeberg #2

Open
nocci wants to merge 26 commits from dev into main
1 changed files with 123 additions and 19 deletions
Showing only changes of commit 602ddc7143 - Show all commits

142
setup.sh
View File

@ -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