added basic admin auth and simple rate limiting
This commit is contained in:
parent
3a7afcdd95
commit
6b1e5b719d
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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">
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
flask
|
||||
flask-caching
|
||||
flask-login
|
||||
flask-oidc
|
||||
flask-limiter
|
||||
pymongo
|
||||
yt-dlp
|
||||
yt-dlp
|
||||
argon2-cffi
|
Loading…
Reference in New Issue