Compare commits

...

2 Commits

Author SHA1 Message Date
Ventilaar
cb2bcc972c Merge branch 'master' of https://git.ventilaar.nl/ventilaar/amazing-ytdlp-archive
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
2024-02-28 00:12:43 +01:00
Ventilaar
6b1e5b719d added basic admin auth and simple rate limiting 2024-02-28 00:12:30 +01:00
11 changed files with 160 additions and 48 deletions

3
.gitignore vendored
View File

@@ -142,3 +142,6 @@ cython_debug/
.idea/ .idea/
/learning/api testing/youtube_api_key.py /learning/api testing/youtube_api_key.py
/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 os
import secrets
from flask import Flask from flask import Flask
from flask_caching import Cache 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): def create_app(test_config=None):
config = {'MONGO_CONNECTION': os.environ.get('AYTA_MONGOCONNECTION', 'mongodb://root:example@192.168.66.140:27017'), 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_ACCESSKEY': os.environ.get('AYTA_S3ACCESSKEY', 'lnUiGClFVXVuZbsr'),
'S3_SECRETKEY': os.environ.get('AYTA_S3SECRETKEY', 'Qz9NG7rpcOWdK2WL'), 'S3_SECRETKEY': os.environ.get('AYTA_S3SECRETKEY', 'Qz9NG7rpcOWdK2WL'),
'CACHE_TYPE': os.environ.get('AYTA_CACHETYPE', 'SimpleCache'), '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 = Flask(__name__)
app.config.from_mapping(config) 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 watch
from .blueprints import index from .blueprints import index
from .blueprints import admin from .blueprints import admin
from .blueprints import search from .blueprints import search
from .blueprints import channel from .blueprints import channel
from .blueprints import auth
app.register_blueprint(watch.bp) app.register_blueprint(watch.bp)
app.register_blueprint(index.bp) app.register_blueprint(index.bp)
app.register_blueprint(admin.bp) app.register_blueprint(admin.bp)
app.register_blueprint(search.bp) app.register_blueprint(search.bp)
app.register_blueprint(channel.bp) app.register_blueprint(channel.bp)
app.register_blueprint(auth.bp)
app.add_url_rule("/", endpoint="base") app.add_url_rule("/", endpoint="base")
return app return app

View File

@@ -10,14 +10,17 @@ from flask import url_for
from ..nosql import get_nosql from ..nosql import get_nosql
from ..s3 import get_s3 from ..s3 import get_s3
from ..dlp import checkChannelId, getChannelInfo from ..dlp import checkChannelId, getChannelInfo
from ..decorators import login_required
bp = Blueprint('admin', __name__, url_prefix='/admin') bp = Blueprint('admin', __name__, url_prefix='/admin')
@bp.route('') @bp.route('')
@login_required
def base(): def base():
return render_template('admin/index.html') return render_template('admin/index.html')
@bp.route('/channels', methods=['GET', 'POST']) @bp.route('/channels', methods=['GET', 'POST'])
@login_required
def channels(): def channels():
channels = {} channels = {}
generic = {} generic = {}
@@ -49,6 +52,7 @@ def channels():
return render_template('admin/channels.html', channels=channels, generic=generic) return render_template('admin/channels.html', channels=channels, generic=generic)
@bp.route('/channel/<channelId>', methods=['GET', 'POST']) @bp.route('/channel/<channelId>', methods=['GET', 'POST'])
@login_required
def channel(channelId): def channel(channelId):
if request.method == 'POST': if request.method == 'POST':
task = request.form.get('task', None) task = request.form.get('task', None)
@@ -56,11 +60,9 @@ def channel(channelId):
value = request.form.get('value', None) value = request.form.get('value', None)
if task == 'update-value': if task == 'update-value':
if key == 'active' and value is None: # html checkbox is not present if not checked if key == 'active':
value = False value = True if value else False
elif key == 'active' and value is not None: # html checkbox is False if checked
value = True
if key == 'added_date': if key == 'added_date':
value = datetime.strptime(value, '%Y-%m-%d') value = datetime.strptime(value, '%Y-%m-%d')
@@ -77,6 +79,7 @@ def channel(channelId):
return render_template('admin/channel.html', channelInfo=channelInfo) return render_template('admin/channel.html', channelInfo=channelInfo)
@bp.route('/runs', methods=['GET', 'POST']) @bp.route('/runs', methods=['GET', 'POST'])
@login_required
def runs(): def runs():
if request.method == 'POST': if request.method == 'POST':
task = request.form.get('task', None) task = request.form.get('task', None)
@@ -90,11 +93,13 @@ def runs():
return render_template('admin/runs.html', runs=runs) return render_template('admin/runs.html', runs=runs)
@bp.route('/run/<runId>', methods=['GET', 'POST']) @bp.route('/run/<runId>', methods=['GET', 'POST'])
@login_required
def run(runId): def run(runId):
run = get_nosql().get_run(runId) run = get_nosql().get_run(runId)
return render_template('admin/run.html', run=run) return render_template('admin/run.html', run=run)
@bp.route('/files', methods=['GET', 'POST']) @bp.route('/files', methods=['GET', 'POST'])
@login_required
def files(): def files():
run = get_s3().list_objects() run = get_s3().list_objects()
return str(run) 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 render_template
from flask import request from flask import request
from flask import url_for from flask import url_for
from flask import flash
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from werkzeug.security import generate_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' %} {% extends 'material_base.html' %}
{% block title %}Admin page | AYTA{% endblock %} {% block title %}Administration page | AYTA{% endblock %}
{% block description %}Administration page of the AYTA system{% endblock %} {% block description %}Administration page of the AYTA system{% endblock %}
{% block content %} {% block content %}
<div class="container channels"> <div class="row">
<div class="head"> <div class="col s12">
<div class="title"> <h4>Administration page</h4>
<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> </div>
<div class="channels flex-grid"> </div>
<div class="card" data-search="system"> <div class="divider"></div>
<a href="{{ url_for('admin.channels') }}" class="inner"> <div class="row">
<div class="content"> <div class="col s12">
<div class="title">System</div> <h5>Global channel options</h5>
<div class="meta">Internal system settings</div> </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> </div>
</a> </div>
</div> </a>
<div class="card" data-search="channels"> </div>
<a href="{{ url_for('admin.channels') }}" class="inner"> <div class="col s6 l4 m-4">
<div class="content"> <a href="{{ url_for('admin.channels') }}">
<div class="title">Channels</div> <div class="card black-text">
<div class="meta">Manage channels in the system</div> <div class="card-content">
<span class="card-title">Channels</span>
<p class="grey-text">Manage channels in the system</p>
</div> </div>
</a> </div>
</div> </a>
<div class="card" data-search="runs"> </div>
<a href="{{ url_for('admin.runs') }}" class="inner"> <div class="col s6 l4 m-4">
<div class="content"> <a href="{{ url_for('admin.runs') }}">
<div class="title">Archive runs</div> <div class="card black-text">
<div class="meta">Look at the cron run logs</div> <div class="card-content">
<span class="card-title">Archive runs</span>
<p class="grey-text">Look at the cron run logs</p>
</div> </div>
</a> </div>
</div> </a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -24,7 +24,7 @@
<div class="card medium black-text"> <div class="card medium black-text">
<a href="{{ url_for('watch.base') }}?v={{ video }}"> <a href="{{ url_for('watch.base') }}?v={{ video }}">
<div class="card-image"> <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> </div>
</a> </a>
<div class="card-content activator"> <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> </ul>
<a href="{{ url_for('index.base') }}" class="brand-logo center">AYTA</a> <a href="{{ url_for('index.base') }}" class="brand-logo center">AYTA</a>
<ul id="nav-mobile" class="right"> <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('search.base') }}">Search</a></li>
<li><a href="{{ url_for('admin.base') }}">Help</a></li> <li><a href="{{ url_for('admin.base') }}">Help</a></li>
</ul> </ul>
@@ -29,6 +32,13 @@
</nav> </nav>
</header> </header>
<main> <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"> <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> <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 %} {% block content %}{% endblock %}

View File

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