added basic admin auth and simple rate limiting

This commit is contained in:
Ventilaar 2024-02-28 00:12:30 +01:00
parent 3a7afcdd95
commit 6b1e5b719d
No known key found for this signature in database
11 changed files with 160 additions and 48 deletions

3
.gitignore vendored
View File

@ -142,3 +142,6 @@ cython_debug/
.idea/
/learning/api testing/youtube_api_key.py
/learning/api testing/youtube_api_key.py
# secrets
client_secrets.json

View File

@ -1,9 +1,11 @@
import os
import secrets
from flask import Flask
from flask_caching import Cache
from .filters import pretty_time
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from . import filters
def create_app(test_config=None):
config = {'MONGO_CONNECTION': os.environ.get('AYTA_MONGOCONNECTION', 'mongodb://root:example@192.168.66.140:27017'),
@ -11,25 +13,36 @@ def create_app(test_config=None):
'S3_ACCESSKEY': os.environ.get('AYTA_S3ACCESSKEY', 'lnUiGClFVXVuZbsr'),
'S3_SECRETKEY': os.environ.get('AYTA_S3SECRETKEY', 'Qz9NG7rpcOWdK2WL'),
'CACHE_TYPE': os.environ.get('AYTA_CACHETYPE', 'SimpleCache'),
'CACHE_DEFAULT_TIMEOUT': os.environ.get('AYTA_CACHETIMEOUT', 300)
'CACHE_DEFAULT_TIMEOUT': os.environ.get('AYTA_CACHETIMEOUT', 300),
'SECRET_KEY': os.environ.get('AYTA_SECRETKEY', secrets.token_hex(32))
}
app = Flask(__name__)
app.config.from_mapping(config)
app.jinja_env.filters['pretty_time'] = pretty_time
limiter = Limiter(
get_remote_address,
app=app,
default_limits=['1000 per day', '100 per hour'],
storage_uri="memory://",
)
app.jinja_env.filters['pretty_time'] = filters.pretty_time
from .blueprints import watch
from .blueprints import index
from .blueprints import admin
from .blueprints import search
from .blueprints import channel
from .blueprints import auth
app.register_blueprint(watch.bp)
app.register_blueprint(index.bp)
app.register_blueprint(admin.bp)
app.register_blueprint(search.bp)
app.register_blueprint(channel.bp)
app.register_blueprint(auth.bp)
app.add_url_rule("/", endpoint="base")
return app

View File

@ -10,14 +10,17 @@ from flask import url_for
from ..nosql import get_nosql
from ..s3 import get_s3
from ..dlp import checkChannelId, getChannelInfo
from ..decorators import login_required
bp = Blueprint('admin', __name__, url_prefix='/admin')
@bp.route('')
@login_required
def base():
return render_template('admin/index.html')
@bp.route('/channels', methods=['GET', 'POST'])
@login_required
def channels():
channels = {}
generic = {}
@ -49,6 +52,7 @@ def channels():
return render_template('admin/channels.html', channels=channels, generic=generic)
@bp.route('/channel/<channelId>', methods=['GET', 'POST'])
@login_required
def channel(channelId):
if request.method == 'POST':
task = request.form.get('task', None)
@ -56,11 +60,9 @@ def channel(channelId):
value = request.form.get('value', None)
if task == 'update-value':
if key == 'active' and value is None: # html checkbox is not present if not checked
value = False
elif key == 'active' and value is not None: # html checkbox is False if checked
value = True
if key == 'active':
value = True if value else False
if key == 'added_date':
value = datetime.strptime(value, '%Y-%m-%d')
@ -77,6 +79,7 @@ def channel(channelId):
return render_template('admin/channel.html', channelInfo=channelInfo)
@bp.route('/runs', methods=['GET', 'POST'])
@login_required
def runs():
if request.method == 'POST':
task = request.form.get('task', None)
@ -90,11 +93,13 @@ def runs():
return render_template('admin/runs.html', runs=runs)
@bp.route('/run/<runId>', methods=['GET', 'POST'])
@login_required
def run(runId):
run = get_nosql().get_run(runId)
return render_template('admin/run.html', run=run)
@bp.route('/files', methods=['GET', 'POST'])
@login_required
def files():
run = get_s3().list_objects()
return str(run)

44
ayta/blueprints/auth.py Normal file
View File

@ -0,0 +1,44 @@
from flask import Blueprint, redirect, url_for, render_template, request, session, flash
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
corr = '$argon2id$v=19$m=64,t=3,p=4$YmY5RTV0bU9tRkx3Q0FvUw$VfPI6BowKvsO4pI1aRslXfbigerssHrHQnQNDhgR8Og'
bp = Blueprint('auth', __name__, url_prefix='/auth')
@bp.route('')
def base():
return redirect(url_for('auth.login'))
@bp.route('/logout')
def logout():
session.pop('username', None)
flash('You have been logged out')
return redirect(url_for('index.base'))
@bp.route('/login', methods=['GET', 'POST'])
#@app.limit('10 per day', override_defaults=False)
def login():
if request.method == 'POST':
username = request.form.get('username', None)
password = request.form.get('password', None)
if not password:
flash('Password was empty')
return 'password required!'
try:
ph = PasswordHasher()
if ph.verify(corr, password):
session['username'] = 'admin'
flash('You have been logged in')
return redirect(url_for('admin.base'))
except VerifyMismatchError:
flash('Wrong password')
except:
flash('Something went wrong')
return render_template('login.html')

View File

@ -2,6 +2,7 @@ from flask import Blueprint
from flask import render_template
from flask import request
from flask import url_for
from flask import flash
from werkzeug.security import check_password_hash
from werkzeug.security import generate_password_hash

11
ayta/decorators.py Normal file
View File

@ -0,0 +1,11 @@
from functools import wraps
from flask import session, request, redirect, url_for, flash
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'username' in session:
return f(*args, **kwargs)
flash('Login required')
return redirect(url_for('auth.login', next=request.url))
return decorated_function

View File

@ -1,46 +1,49 @@
{% extends 'base.html' %}
{% block title %}Admin page | AYTA{% endblock %}
{% extends 'material_base.html' %}
{% block title %}Administration page | AYTA{% endblock %}
{% block description %}Administration page of the AYTA system{% endblock %}
{% block content %}
<div class="container channels">
<div class="head">
<div class="title">
<h1 style="display: inline-block;">Administration page</h1>
</div>
<div>
<select name="sort" class="sort{sort}" oninput="channelSort()">
<option value="name-a">Name (asc)</option>
<option value="name-d">Name (desc)</option>
</select>
<input type="text" class="search" oninput="channelSearch()" placeholder="Search...">
</div>
<div class="row">
<div class="col s12">
<h4>Administration page</h4>
</div>
<div class="channels flex-grid">
<div class="card" data-search="system">
<a href="{{ url_for('admin.channels') }}" class="inner">
<div class="content">
<div class="title">System</div>
<div class="meta">Internal system settings</div>
</div>
<div class="divider"></div>
<div class="row">
<div class="col s12">
<h5>Global channel options</h5>
</div>
</div>
<div class="row">
<div class="col s6 l4 m-4">
<a href="{{ url_for('admin.channels') }}">
<div class="card black-text">
<div class="card-content">
<span class="card-title">System</span>
<p class="grey-text">Internal system settings</p>
</div>
</a>
</div>
<div class="card" data-search="channels">
<a href="{{ url_for('admin.channels') }}" class="inner">
<div class="content">
<div class="title">Channels</div>
<div class="meta">Manage channels in the system</div>
</div>
</a>
</div>
<div class="col s6 l4 m-4">
<a href="{{ url_for('admin.channels') }}">
<div class="card black-text">
<div class="card-content">
<span class="card-title">Channels</span>
<p class="grey-text">Manage channels in the system</p>
</div>
</a>
</div>
<div class="card" data-search="runs">
<a href="{{ url_for('admin.runs') }}" class="inner">
<div class="content">
<div class="title">Archive runs</div>
<div class="meta">Look at the cron run logs</div>
</div>
</a>
</div>
<div class="col s6 l4 m-4">
<a href="{{ url_for('admin.runs') }}">
<div class="card black-text">
<div class="card-content">
<span class="card-title">Archive runs</span>
<p class="grey-text">Look at the cron run logs</p>
</div>
</a>
</div>
</div>
</a>
</div>
</div>
{% endblock %}

View File

@ -24,7 +24,7 @@
<div class="card medium black-text">
<a href="{{ url_for('watch.base') }}?v={{ video }}">
<div class="card-image">
<img loading="lazy" src="{{ url_for('static', filename='img/logo_text.png') }}">
<img loading="lazy" src="https://archive.ventilaar.net/videos/automatic/{{ channel.get('id') }}/{{ videos[video].get('id') }}/{{ videos[video].get('title') }}.webp">
</div>
</a>
<div class="card-content activator">

18
ayta/templates/login.html Normal file
View File

@ -0,0 +1,18 @@
{% extends 'material_base.html' %}
{% block title %}Login Page | AYTA{% endblock %}
{% block description %}Login required for the requested page{% endblock %}
{% block content %}
<div class="row">
<div class="col s3">
<h4>pls login</h4>
<form method="post">
<div class="input-field">
<input required placeholder="Password" name="password" type="password" class="validate">
</div>
<button class="btn mt-4" type="submit" name="action" value="login">Login</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -22,6 +22,9 @@
</ul>
<a href="{{ url_for('index.base') }}" class="brand-logo center">AYTA</a>
<ul id="nav-mobile" class="right">
{% if 'username' in session %}
<li><a href="{{ url_for('auth.logout') }}">Logged in as {{ session.username }} (click to logout)</a></li>
{% endif %}
<li><a href="{{ url_for('search.base') }}">Search</a></li>
<li><a href="{{ url_for('admin.base') }}">Help</a></li>
</ul>
@ -29,6 +32,13 @@
</nav>
</header>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<script>M.toast({text: '{{ message }}', displayLength: 5000, outDuration: 999, inDuration: 666})</script>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container">
<noscript>Hey there, while I did build this in mind to minimize javascript usage, the experience would be much better if you would enable it!</noscript>
{% block content %}{% endblock %}

View File

@ -1,4 +1,8 @@
flask
flask-caching
flask-login
flask-oidc
flask-limiter
pymongo
yt-dlp
yt-dlp
argon2-cffi