From f8a314df4f6c2b467c4fb9b761beabb09f925589 Mon Sep 17 00:00:00 2001 From: A nyaa developer Date: Sat, 26 Aug 2017 00:53:35 +0200 Subject: [PATCH] Better bans (#341) * better bans * put jinja2 template into correct file --- migrations/versions/500117641608_add_bans.py | 38 +++++++ nyaa/forms.py | 46 ++++++++ nyaa/models.py | 69 +++++++++-- nyaa/static/css/main.css | 4 + nyaa/static/js/main.js | 9 +- nyaa/template_utils.py | 7 ++ nyaa/templates/admin_bans.html | 55 +++++++++ nyaa/templates/adminlog.html | 6 +- nyaa/templates/edit.html | 112 ++++++++++++++++-- nyaa/templates/layout.html | 1 + nyaa/templates/user.html | 81 ++++++++++--- nyaa/views/account.py | 8 +- nyaa/views/admin.py | 43 ++++++- nyaa/views/main.py | 14 ++- nyaa/views/torrents.py | 114 +++++++++++++++---- nyaa/views/users.py | 67 +++++++++-- 16 files changed, 609 insertions(+), 65 deletions(-) create mode 100644 migrations/versions/500117641608_add_bans.py create mode 100644 nyaa/templates/admin_bans.html diff --git a/migrations/versions/500117641608_add_bans.py b/migrations/versions/500117641608_add_bans.py new file mode 100644 index 0000000..d26f4ec --- /dev/null +++ b/migrations/versions/500117641608_add_bans.py @@ -0,0 +1,38 @@ +"""Add bans table + +Revision ID: 500117641608 +Revises: b79d2fcafd88 +Create Date: 2017-08-17 01:44:39.205126 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '500117641608' +down_revision = 'b79d2fcafd88' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('bans', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_time', sa.DateTime(), nullable=True), + sa.Column('admin_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('user_ip', sa.Binary(length=16), nullable=True), + sa.Column('reason', sa.String(length=2048), nullable=False), + sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('user_ip_16', 'bans', ['user_ip'], unique=True, mysql_length=16) + op.create_index('user_ip_4', 'bans', ['user_ip'], unique=True, mysql_length=4) + + +def downgrade(): + op.drop_index('user_ip_4', table_name='bans') + op.drop_index('user_ip_16', table_name='bans') + op.drop_table('bans') diff --git a/nyaa/forms.py b/nyaa/forms.py index bf0b2cf..15d1557 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -148,6 +148,33 @@ class CommentForm(FlaskForm): ]) +class InlineButtonWidget(object): + """ + Render a basic `` + {% else %} + {% set text = "Ban User" %} + {% if not torrent.banned %} + {% set text = "Ban and Ban User" %} + {% if not torrent.deleted %} + {% set text = "Delete & Ban and Ban User" %} + {% endif %} + {% endif %} + + {{ ban_form.ban_user(class="btn btn-danger") }} + {% endif %} + +
+ {% if ipbanned %} + + {% else %} + {% set text = "Ban User+IP" %} + {% if not torrent.banned %} + {% set text = "Ban and Ban User+IP" %} + {% if not torrent.deleted %} + {% set text = "Delete & Ban and Ban User+IP" %} + {% endif %} + {% endif %} + + {{ ban_form.ban_userip(value=text, class="btn btn-danger") }} + {% endif %} +
+ {% else %} +
+ +
+
+ {% if ipbanned %} + + {% else %} + {% set text = "Ban IP" %} + {% if not torrent.banned %} + {% set text = "Ban and Ban IP" %} + {% if not torrent.deleted %} + {% set text = "Delete & Ban and Ban IP" %} + {% endif %} + {% endif %} + + {{ ban_form.ban_userip(value=text, class="btn btn-danger") }} + {% endif %} +
+ {% endif %} +{% endif %} +{% endif %} diff --git a/nyaa/templates/layout.html b/nyaa/templates/layout.html index 43da9cb..5bea4d4 100644 --- a/nyaa/templates/layout.html +++ b/nyaa/templates/layout.html @@ -97,6 +97,7 @@ {% endif %} diff --git a/nyaa/templates/user.html b/nyaa/templates/user.html index 35691c5..0de97d3 100644 --- a/nyaa/templates/user.html +++ b/nyaa/templates/user.html @@ -11,14 +11,15 @@ {% block body %} {% from "_formhelpers.html" import render_menu_with_button %} +{% from "_formhelpers.html" import render_field %} {% if g.user and g.user.is_moderator %}

User Information


-
+
-
+
User ID:
{{ user.id }}
@@ -35,20 +36,74 @@
{{ user.ip_string }}

{%- endif -%}
-
-
- {% if admin_form %} -
- {{ admin_form.csrf_token }} + {% if admin_form %} + + {{ admin_form.csrf_token }} -
-
- {{ render_menu_with_button(admin_form.user_class) }} +
+ {{ render_menu_with_button(admin_form.user_class) }} +
+ +
+ {% endif %} +
+ {% if ban_form %} +
+
+
+

Danger Zone

+
+
+
+ {{ ban_form.csrf_token }} + {% if user.is_banned %} + This user is banned.
+ {% endif %} + {% if ipbanned %} + This user is ip banned.
+ {% endif %} + {% if not user.is_banned and not bans %} + This user is not banned.
+ {% endif %} +
+ {% if user.is_banned or bans %} +

+ {% for ban in bans %} + #{{ ban.id }} + by {{ ban.admin.username }} + for {{ ban.reason }} +
+ {% endfor %} +

+ {{ ban_form.unban(class="btn btn-info") }} + {% endif %} + + {% if not user.is_banned or not ipbanned %} + {% if user.is_banned or bans %} +
+ {% endif %} + {{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}
+
+ {% if not user.is_banned %} + {{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }} + {% else %} + + {% endif %} +
+
+ {% if not ipbanned %} + {{ ban_form.ban_userip(value="Ban User+IP", class="btn btn-danger") }} + {% else %} + + {% endif %} +
+ {% endif %} +
+
- -
- {% endif %} + {% endif %} +
{% endif %}

diff --git a/nyaa/views/account.py b/nyaa/views/account.py index f3dd182..5e9615a 100644 --- a/nyaa/views/account.py +++ b/nyaa/views/account.py @@ -28,12 +28,16 @@ def login(): if not user: user = models.User.by_email(username) - if (not user or password != user.password_hash or - user.status == models.UserStatusType.INACTIVE): + if not user or password != user.password_hash: flask.flash(flask.Markup( 'Login failed! Incorrect username or password.'), 'danger') return flask.redirect(flask.url_for('account.login')) + if user.status != models.UserStatusType.ACTIVE: + flask.flash(flask.Markup( + 'Login failed! Account is not activated or banned.'), 'danger') + return flask.redirect(flask.url_for('account.login')) + user.last_login_date = datetime.utcnow() user.last_login_ip = ip_address(flask.request.remote_addr).packed db.session.add(user) diff --git a/nyaa/views/admin.py b/nyaa/views/admin.py index 9e412dd..0d36343 100644 --- a/nyaa/views/admin.py +++ b/nyaa/views/admin.py @@ -1,3 +1,5 @@ +from ipaddress import ip_address + import flask from nyaa import forms, models @@ -20,6 +22,45 @@ def view_adminlog(): adminlog=logs) +@bp.route('/bans', endpoint='bans', methods=['GET', 'POST']) +def view_adminbans(): + if not flask.g.user or not flask.g.user.is_moderator: + flask.abort(403) + + form = forms.StringSubmitForm() + if flask.request.method == 'POST' and form.validate(): + ban = models.Ban.by_id(form.submit.data) + if not ban: + flask.abort(404) + + log = 'Unbanned ban #{0}'.format(ban.id) + + if ban.user: + log += ' ' + ban.user.username + ban.user.status = models.UserStatusType.ACTIVE + db.session.add(ban.user) + + if ban.user_ip: + log += ' IP({0})'.format(ip_address(ban.user_ip)) + + adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id) + db.session.add(adminlog) + + db.session.delete(ban) + db.session.commit() + + flask.flash('Unbanned ban #{0}'.format(ban.id), 'success') + + page = flask.request.args.get('p', flask.request.args.get('offset', 1, int), int) + bans = models.Ban.all_bans() \ + .order_by(models.Ban.created_time.desc()) \ + .paginate(page=page, per_page=20) + + return flask.render_template('admin_bans.html', + bans=bans, + form=form) + + @bp.route('/reports', endpoint='reports', methods=['GET', 'POST']) def view_reports(): if not flask.g.user or not flask.g.user.is_moderator: @@ -40,7 +81,7 @@ def view_reports(): if not torrent or not report or report.status != 0: flask.abort(404) else: - log = "Report #{}: {} [#{}]({}), reported by [{}]({})" + log = 'Report #{}: {} [#{}]({}), reported by [{}]({})' if action == 'delete': torrent.deleted = True report.status = 1 diff --git a/nyaa/views/main.py b/nyaa/views/main.py index f9c54e0..e82e7ac 100644 --- a/nyaa/views/main.py +++ b/nyaa/views/main.py @@ -1,6 +1,7 @@ import math import re from datetime import datetime, timedelta +from ipaddress import ip_address import flask from flask_paginate import Pagination @@ -28,6 +29,10 @@ def before_request(): if not user: return logout() + # Logout inactive and banned users + if user.status != models.UserStatusType.ACTIVE: + return logout() + flask.g.user = user if 'timeout' not in flask.session or flask.session['timeout'] < datetime.now(): @@ -35,7 +40,14 @@ def before_request(): flask.session.permanent = True flask.session.modified = True - if flask.g.user.status == models.UserStatusType.BANNED: + # Check if user is banned on POST + if flask.request.method == 'POST': + ip = ip_address(flask.request.remote_addr).packed + banned = models.Ban.banned(None, ip).first() + if banned: + if flask.g.user: + return logout() + return 'You are banned.', 403 diff --git a/nyaa/views/torrents.py b/nyaa/views/torrents.py index 8a267af..9fec0c0 100644 --- a/nyaa/views/torrents.py +++ b/nyaa/views/torrents.py @@ -1,5 +1,6 @@ import json import os.path +from ipaddress import ip_address from urllib.parse import quote import flask @@ -80,8 +81,9 @@ def view_torrent(torrent_id): def edit_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) form = forms.EditForm(flask.request.form) - delete_form = forms.DeleteForm() form.category.choices = _create_upload_category_choices() + delete_form = forms.DeleteForm() + ban_form = None editor = flask.g.user @@ -96,7 +98,10 @@ def edit_torrent(torrent_id): if not editor or not (editor is torrent.user or editor.is_moderator): flask.abort(403) - if flask.request.method == 'POST' and form.validate(): + if editor and editor.is_moderator and editor.level > torrent.user.level: + ban_form = forms.BanForm() + + if flask.request.method == 'POST' and form.submit.data and form.validate(): # Form has been sent, edit torrent with data. torrent.main_category_id, torrent.sub_category_id = \ form.category.parsed_data.get_category_ids() @@ -127,9 +132,12 @@ def edit_torrent(torrent_id): flask.flash(flask.Markup( 'Torrent has been successfully edited! Changes might take a few minutes to show up.'), - 'info') + 'success') return flask.redirect(url) + elif flask.request.method == 'POST' and delete_form.validate() and \ + (not ban_form or ban_form.validate()): + return _delete_torrent(torrent, delete_form, ban_form) else: if flask.request.method != 'POST': # Fill form data only if the POST didn't fail @@ -146,39 +154,43 @@ def edit_torrent(torrent_id): form.is_trusted.data = torrent.trusted form.is_deleted.data = torrent.deleted + ipbanned = None + if editor.is_moderator: + tbanned = models.Ban.banned(None, torrent.uploader_ip).first() + ubanned = True + if torrent.user: + ubanned = models.Ban.banned(None, torrent.user.last_login_ip).first() + ipbanned = (tbanned and ubanned) + return flask.render_template('edit.html', form=form, delete_form=delete_form, - torrent=torrent) + ban_form=ban_form, + torrent=torrent, + ipbanned=ipbanned) -@bp.route('/view//delete', endpoint='delete', methods=['POST']) -def delete_torrent(torrent_id): - torrent = models.Torrent.by_id(torrent_id) - form = forms.DeleteForm(flask.request.form) - +def _delete_torrent(torrent, form, banform): editor = flask.g.user - - if not torrent: - flask.abort(404) + uploader = torrent.user # Only allow admins edit deleted torrents if torrent.deleted and not (editor and editor.is_moderator): flask.abort(404) - # Only allow torrent owners or admins edit torrents - if not editor or not (editor is torrent.user or editor.is_moderator): - flask.abort(403) - action = None url = flask.url_for('main.home') + ban_torrent = form.ban.data + if banform: + ban_torrent = ban_torrent or banform.ban_user.data or banform.ban_userip.data + if form.delete.data and not torrent.deleted: action = 'deleted' torrent.deleted = True db.session.add(torrent) - elif form.ban.data and not torrent.banned and editor.is_moderator: + elif ban_torrent and not torrent.banned and editor.is_moderator: action = 'banned' torrent.banned = True if not torrent.deleted: @@ -202,20 +214,80 @@ def delete_torrent(torrent_id): backend.tracker_api([torrent.info_hash], 'unban') db.session.add(torrent) - if not action: + if not action and not ban_torrent: flask.flash(flask.Markup('What the fuck are you doing?'), 'danger') return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) - if editor.is_moderator: + if action and editor.is_moderator: url = flask.url_for('torrents.view', torrent_id=torrent.id) - if editor is not torrent.user: + if editor is not uploader: log = "Torrent [#{0}]({1}) has been {2}".format(torrent.id, url, action) adminlog = models.AdminLog(log=log, admin_id=editor.id) db.session.add(adminlog) + if action: + db.session.commit() + flask.flash(flask.Markup('Torrent has been successfully {0}.'.format(action)), 'success') + + if not banform or not (banform.ban_user.data or banform.ban_userip.data): + return flask.redirect(url) + + if banform.ban_userip.data: + tbanned = models.Ban.banned(None, torrent.uploader_ip).first() + ubanned = True + if uploader: + ubanned = models.Ban.banned(None, uploader.last_login_ip).first() + ipbanned = (tbanned and ubanned) + + if (banform.ban_user.data and (not uploader or uploader.is_banned)) or \ + (banform.ban_userip.data and ipbanned): + flask.flash(flask.Markup('What the fuck are you doing?'), 'danger') + return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) + + flavor = "Nyaa" if app.config['SITE_FLAVOR'] == 'nyaa' else "Sukebei" + eurl = flask.url_for('torrents.view', torrent_id=torrent.id, _external=True) + reason = "[{0}#{1}]({2}) {3}".format(flavor, torrent.id, eurl, banform.reason.data) + ban1 = models.Ban(admin_id=editor.id, reason=reason) + ban2 = models.Ban(admin_id=editor.id, reason=reason) + db.session.add(ban1) + + if uploader: + uploader.status = models.UserStatusType.BANNED + db.session.add(uploader) + ban1.user_id = uploader.id + ban2.user_id = uploader.id + + if banform.ban_userip.data: + if not ubanned: + ban1.user_ip = ip_address(uploader.last_login_ip) + if not tbanned: + uploader_ip = ip_address(torrent.uploader_ip) + if ban1.user_ip != uploader_ip: + ban2.user_ip = uploader_ip + db.session.add(ban2) + else: + ban1.user_ip = ip_address(torrent.uploader_ip) + + uploader_str = "Anonymous" + if uploader: + uploader_url = flask.url_for('users.view_user', user_name=uploader.username) + uploader_str = "[{0}]({1})".format(uploader.username, uploader_url) + if ban1.user_ip: + uploader_str += " IP({0})".format(ban1.user_ip) + ban1.user_ip = ban1.user_ip.packed + if ban2.user_ip: + uploader_str += " IP({0})".format(ban2.user_ip) + ban2.user_ip = ban2.user_ip.packed + + log = "Uploader {0} of torrent [#{1}]({2}) has been banned.".format( + uploader_str, torrent.id, flask.url_for('torrents.view', torrent_id=torrent.id), action) + adminlog = models.AdminLog(log=log, admin_id=editor.id) + db.session.add(adminlog) + db.session.commit() - flask.flash(flask.Markup('Torrent has been successfully {0}.'.format(action)), 'info') + flask.flash(flask.Markup('Uploader has been successfully banned.'), 'success') + return flask.redirect(url) diff --git a/nyaa/views/users.py b/nyaa/views/users.py index 539f296..31b9861 100644 --- a/nyaa/views/users.py +++ b/nyaa/views/users.py @@ -1,4 +1,5 @@ import math +from ipaddress import ip_address import flask from flask_paginate import Pagination @@ -23,14 +24,23 @@ def view_user(user_name): flask.abort(404) admin_form = None + ban_form = None + bans = None + ipbanned = None if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level: admin_form = forms.UserForm() default, admin_form.user_class.choices = _create_user_class_choices(user) if flask.request.method == 'GET': admin_form.user_class.data = default + ban_form = forms.BanForm() + if flask.request.method == 'POST': + doban = (ban_form.ban_user.data or ban_form.unban.data or ban_form.ban_userip.data) + bans = models.Ban.banned(user.id, user.last_login_ip).all() + ipbanned = list(filter(lambda b: b.user_ip == user.last_login_ip, bans)) + url = flask.url_for('users.view_user', user_name=user.username) - if flask.request.method == 'POST' and admin_form and admin_form.validate(): + if flask.request.method == 'POST' and admin_form and not doban and admin_form.validate(): selection = admin_form.user_class.data log = None if selection == 'regular': @@ -42,9 +52,6 @@ def view_user(user_name): elif selection == 'moderator': user.level = models.UserLevelType.MODERATOR log = "[{}]({}) changed to moderator user".format(user_name, url) - elif selection == 'banned': - user.status = models.UserStatusType.BANNED - log = "[{}]({}) changed to banned user".format(user_name, url) adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id) db.session.add(user) @@ -53,6 +60,46 @@ def view_user(user_name): return flask.redirect(url) + if flask.request.method == 'POST' and ban_form and doban and ban_form.validate(): + if (ban_form.ban_user.data and user.is_banned) or \ + (ban_form.ban_userip.data and ipbanned) or \ + (ban_form.unban.data and not user.is_banned and not bans): + flask.flash(flask.Markup('What the fuck are you doing?'), 'danger') + return flask.redirect(url) + + user_str = "[{0}]({1})".format(user.username, url) + + if ban_form.unban.data: + action = "unbanned" + user.status = models.UserStatusType.ACTIVE + db.session.add(user) + + for ban in bans: + if ban.user_ip: + user_str += " IP({0})".format(ip_address(ban.user_ip)) + db.session.delete(ban) + else: + action = "banned" + user.status = models.UserStatusType.BANNED + db.session.add(user) + + ban = models.Ban(admin_id=flask.g.user.id, user_id=user.id, reason=ban_form.reason.data) + db.session.add(ban) + + if ban_form.ban_userip.data: + ban.user_ip = ip_address(user.last_login_ip) + user_str += " IP({0})".format(ban.user_ip) + ban.user_ip = ban.user_ip.packed + + log = "User {0} has been {1}.".format(user_str, action) + adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id) + db.session.add(adminlog) + + db.session.commit() + + flask.flash(flask.Markup('User has been successfully {0}.'.format(action)), 'success') + return flask.redirect(url) + req_args = flask.request.args search_term = chain_get(req_args, 'q', 'term') @@ -117,7 +164,10 @@ def view_user(user_name): user=user, user_page=True, rss_filter=rss_query_string, - admin_form=admin_form) + admin_form=admin_form, + ban_form=ban_form, + bans=bans, + ipbanned=ipbanned) # Similar logic as home page else: if use_elastic: @@ -132,7 +182,10 @@ def view_user(user_name): user=user, user_page=True, rss_filter=rss_query_string, - admin_form=admin_form) + admin_form=admin_form, + ban_form=ban_form, + bans=bans, + ipbanned=ipbanned) @bp.route('/user/activate/') @@ -164,8 +217,6 @@ def _create_user_class_choices(user): choices.append(('trusted', 'Trusted')) if flask.g.user.is_superadmin: choices.append(('moderator', 'Moderator')) - if flask.g.user.is_moderator: - choices.append(('banned', 'Banned')) if user: if user.is_moderator: