update readme due release on codeberg #2
134
setup.sh
134
setup.sh
|
@ -182,7 +182,8 @@ from flask import (
|
||||||
send_file,
|
send_file,
|
||||||
jsonify,
|
jsonify,
|
||||||
Markup,
|
Markup,
|
||||||
make_response
|
make_response,
|
||||||
|
abort
|
||||||
)
|
)
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
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
|
from sqlalchemy.engine import Engine
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
@event.listens_for(Engine, "connect")
|
@event.listens_for(Engine, "connect")
|
||||||
def enable_foreign_keys(dbapi_connection, connection_record):
|
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
|
value = translations.get(key) or fallback_translations.get(key) or key
|
||||||
return value.format(**kwargs) if isinstance(value, str) else value
|
return value.format(**kwargs) if isinstance(value, str) else value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## DEBUG Translations
|
## DEBUG Translations
|
||||||
if app.debug:
|
if app.debug:
|
||||||
print(f"Loaded translations for 'de': {TRANSLATIONS.get('de', {})}")
|
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)
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
convention = {
|
convention = {
|
||||||
|
@ -363,11 +371,12 @@ def _jinja2_filter_datetime(date, fmt='%d.%m.%Y'):
|
||||||
|
|
||||||
# DB Models
|
# DB Models
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
__tablename__ = 'users' # Expliziter Tabellenname
|
__tablename__ = 'users'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
password = db.Column(db.String(256), nullable=False)
|
password = db.Column(db.String(256), nullable=False)
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
games = db.relationship(
|
games = db.relationship(
|
||||||
'Game',
|
'Game',
|
||||||
back_populates='owner',
|
back_populates='owner',
|
||||||
|
@ -526,21 +535,30 @@ def login():
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
if not app.config['REGISTRATION_ENABLED']:
|
if not app.config['REGISTRATION_ENABLED']:
|
||||||
flash(translate('No new registrations. They are deactivated!'), 'danger')
|
abort(403)
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = generate_password_hash(request.form['password'])
|
password = request.form['password']
|
||||||
|
|
||||||
if User.query.filter_by(username=username).first():
|
existing_user = User.query.filter_by(username=username).first()
|
||||||
flash(translate('Username already exists', session.get('lang', 'en')), 'danger')
|
if existing_user:
|
||||||
|
flash(translate('Username already exists'), 'error')
|
||||||
return redirect(url_for('register'))
|
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.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(new_user)
|
login_user(new_user)
|
||||||
|
flash(translate('Registration successful'), 'success')
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
return render_template('register.html')
|
return render_template('register.html')
|
||||||
|
@ -946,6 +964,40 @@ def redeem_page(token):
|
||||||
expires_timestamp=int(expires_utc.timestamp() * 1000), # Millisekunden
|
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')
|
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
|
# Apprise Notifications
|
||||||
import apprise
|
import apprise
|
||||||
|
@ -1059,7 +1111,7 @@ cat <<HTML_END > templates/base.html
|
||||||
<!-- Preload Bootstrap CSS for better LCP -->
|
<!-- 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'">
|
<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>
|
<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') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
{% if games and games[0].steam_appid %}
|
{% if games and games[0].steam_appid %}
|
||||||
<link rel="preload"
|
<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;">
|
<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>
|
<span>Game Key Manager</span>
|
||||||
</a>
|
</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') }}">
|
<form class="d-flex" action="{{ url_for('index') }}" method="GET" role="search" aria-label="{{ _('Search games') }}">
|
||||||
<label for="searchInput" class="visually-hidden">{{ _('Search') }}</label>
|
<label for="searchInput" class="visually-hidden">{{ _('Search') }}</label>
|
||||||
<input class="form-control me-2"
|
<input class="form-control me-2"
|
||||||
|
@ -1117,12 +1169,21 @@ cat <<HTML_END > templates/base.html
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.is_authenticated %}
|
{% 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">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
|
<a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
<a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1132,7 +1193,7 @@ cat <<HTML_END > templates/base.html
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="flash-container">
|
<div class="flash-container">
|
||||||
{% for category, message in messages %}
|
{% 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 }}
|
{{ message|safe }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1833,6 +1894,49 @@ cat <<HTML_END > templates/footer.html
|
||||||
</footer>
|
</footer>
|
||||||
HTML_END
|
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
|
# CSS
|
||||||
cat <<CSS_END > static/style.css
|
cat <<CSS_END > static/style.css
|
||||||
|
|
Loading…
Reference in New Issue