1
0
Fork 0
mirror of https://gitlab.com/SIGBUS/nyaa.git synced 2024-12-22 15:40:00 +00:00

Better bans (#341)

* better bans

* put jinja2 template into correct file
This commit is contained in:
A nyaa developer 2017-08-26 00:53:35 +02:00 committed by Arylide
parent 6aab5557d6
commit f8a314df4f
16 changed files with 609 additions and 65 deletions

View file

@ -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')

View file

@ -148,6 +148,33 @@ class CommentForm(FlaskForm):
]) ])
class InlineButtonWidget(object):
"""
Render a basic ``<button>`` field.
"""
input_type = 'submit'
html_params = staticmethod(html_params)
def __call__(self, field, label=None, **kwargs):
kwargs.setdefault('id', field.id)
kwargs.setdefault('type', self.input_type)
if not label:
label = field.label.text
return HTMLString('<button %s>' % self.html_params(name=field.name, **kwargs) + label)
class StringSubmitField(StringField):
"""
Represents an ``<button type="submit">``. This allows checking if a given
submit button has been pressed.
"""
widget = InlineButtonWidget()
class StringSubmitForm(FlaskForm):
submit = StringSubmitField('Submit')
class EditForm(FlaskForm): class EditForm(FlaskForm):
display_name = StringField('Torrent display name', [ display_name = StringField('Torrent display name', [
Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters ' Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters '
@ -185,6 +212,8 @@ class EditForm(FlaskForm):
Length(max=10 * 1024, message='Description must be at most %(max)d characters long.') Length(max=10 * 1024, message='Description must be at most %(max)d characters long.')
]) ])
submit = SubmitField('Save Changes')
class DeleteForm(FlaskForm): class DeleteForm(FlaskForm):
delete = SubmitField("Delete") delete = SubmitField("Delete")
@ -193,6 +222,23 @@ class DeleteForm(FlaskForm):
unban = SubmitField("Unban") unban = SubmitField("Unban")
class BanForm(FlaskForm):
ban_user = SubmitField("Delete & Ban and Ban User")
ban_userip = SubmitField("Delete & Ban and Ban User+IP")
unban = SubmitField("Unban")
_validator = DataRequired()
def _validate_reason(form, field):
if form.ban_user.data or form.ban_userip.data:
return BanForm._validator(form, field)
reason = TextAreaField('Ban Reason', [
_validate_reason,
Length(max=1024, message='Reason must be at most %(max)d characters long.')
])
class UploadForm(FlaskForm): class UploadForm(FlaskForm):
torrent_file = FileField('Torrent file', [ torrent_file = FileField('Torrent file', [
FileRequired() FileRequired()

View file

@ -484,6 +484,8 @@ class User(db.Model):
sukebei_torrents = db.relationship('SukebeiTorrent', back_populates='user', lazy='dynamic') sukebei_torrents = db.relationship('SukebeiTorrent', back_populates='user', lazy='dynamic')
sukebei_comments = db.relationship('SukebeiComment', back_populates='user', lazy='dynamic') sukebei_comments = db.relationship('SukebeiComment', back_populates='user', lazy='dynamic')
bans = db.relationship('Ban', uselist=True, foreign_keys='Ban.user_id')
def __init__(self, username, email, password): def __init__(self, username, email, password):
self.username = username self.username = username
self.email = email self.email = email
@ -521,14 +523,18 @@ class User(db.Model):
@property @property
def userlevel_str(self): def userlevel_str(self):
level = ''
if self.level == UserLevelType.REGULAR: if self.level == UserLevelType.REGULAR:
return 'User' level = 'User'
elif self.level == UserLevelType.TRUSTED: elif self.level == UserLevelType.TRUSTED:
return 'Trusted' level = 'Trusted'
elif self.level == UserLevelType.MODERATOR: elif self.level == UserLevelType.MODERATOR:
return 'Moderator' level = 'Moderator'
elif self.level >= UserLevelType.SUPERADMIN: elif self.level >= UserLevelType.SUPERADMIN:
return 'Administrator' level = 'Administrator'
if self.is_banned:
level = 'BANNED ' + level
return level
@property @property
def userstatus_str(self): def userstatus_str(self):
@ -541,12 +547,16 @@ class User(db.Model):
@property @property
def userlevel_color(self): def userlevel_color(self):
color = ''
if self.level == UserLevelType.REGULAR: if self.level == UserLevelType.REGULAR:
return 'default' color = 'default'
elif self.level == UserLevelType.TRUSTED: elif self.level == UserLevelType.TRUSTED:
return 'success' color = 'success'
elif self.level >= UserLevelType.MODERATOR: elif self.level >= UserLevelType.MODERATOR:
return 'purple' color = 'purple'
if self.is_banned:
color += ' strike'
return color
@property @property
def ip_string(self): def ip_string(self):
@ -679,6 +689,51 @@ class ReportBase(DeclarativeHelperBase):
return cls.query.filter(cls.torrent_id == id, cls.status == 0).delete() return cls.query.filter(cls.torrent_id == id, cls.status == 0).delete()
class Ban(db.Model):
__tablename__ = 'bans'
id = db.Column(db.Integer, primary_key=True)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
admin_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
user_ip = db.Column(db.Binary(length=16), nullable=True)
reason = db.Column(db.String(length=2048), nullable=False)
admin = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[admin_id])
user = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[user_id])
__table_args__ = (
Index('user_ip_4', 'user_ip', mysql_length=4, unique=True),
Index('user_ip_16', 'user_ip', mysql_length=16, unique=True),
)
def __repr__(self):
return '<Ban %r>' % self.id
@property
def ip_string(self):
if self.user_ip:
return str(ip_address(self.user_ip))
@classmethod
def all_bans(cls):
return cls.query
@classmethod
def by_id(cls, id):
return cls.query.get(id)
@classmethod
def banned(cls, user_id, user_ip):
if user_id:
if user_ip:
return cls.query.filter((cls.user_id == user_id) | (cls.user_ip == user_ip))
return cls.query.filter(cls.user_id == user_id)
if user_ip:
return cls.query.filter(cls.user_ip == user_ip)
return None
# Actually declare our site-specific classes # Actually declare our site-specific classes
# Torrent # Torrent

View file

@ -331,6 +331,10 @@ a.text-purple:hover, a.text-purple:active, a.text-purple:focus {
text-align: center; text-align: center;
} }
.strike {
text-decoration: line-through;
}
/* Torrent file list */ /* Torrent file list */
.torrent-file-list ul { .torrent-file-list ul {
padding: 5px 20px 0px 20px; padding: 5px 20px 0px 20px;

View file

@ -180,10 +180,15 @@ document.addEventListener("DOMContentLoaded", function() {
// Render markdown from elements with "markdown-text" attribute // Render markdown from elements with "markdown-text" attribute
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
var markdownTargets = document.querySelectorAll('[markdown-text]'); var markdownTargets = document.querySelectorAll('[markdown-text],[markdown-text-inline]');
for (var i = 0; i < markdownTargets.length; i++) { for (var i = 0; i < markdownTargets.length; i++) {
var target = markdownTargets[i]; var target = markdownTargets[i];
var rendered = markdown.render(target.innerHTML); var rendered;
if (target.attributes["markdown-text-inline"]) {
rendered = markdown.renderInline(target.innerHTML);
} else {
rendered = markdown.render(target.innerHTML);
}
target.innerHTML = rendered; target.innerHTML = rendered;
} }
}); });

View file

@ -1,4 +1,5 @@
import os.path import os.path
import re
from base64 import b32encode from base64 import b32encode
from datetime import datetime from datetime import datetime
from email.utils import formatdate from email.utils import formatdate
@ -135,3 +136,9 @@ def timesince(dt, default='just now'):
return '%d %s ago' % (period, singular if int(period) == 1 else plural) return '%d %s ago' % (period, singular if int(period) == 1 else plural)
return default return default
@bp.app_template_filter()
def regex_replace(s, find, replace):
"""A non-optimal implementation of a regex filter"""
return re.sub(find, replace, s)

View file

@ -0,0 +1,55 @@
{% extends "layout.html" %}
{% block title %}Admin Log :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
{% from "_formhelpers.html" import render_field %}
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th style="width: 75px">#</th>
<th>Moderator/Admin</th>
<th>User</th>
<th>User IP</th>
<th style="width: 700px">Reason</th>
<th style="width: 175px">Date</th>
<th style="width: 75px">Action</th>
</tr>
</thead>
<tbody>
<form method="POST">
{{ form.csrf_token }}
{% for ban in bans.items %}
<tr>
<td>{{ ban.id }}</td>
<td>
<a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a>
</td>
{% if ban.user %}
<td>
<a href="{{ url_for('users.view_user', user_name=ban.user.username) }}">{{ ban.user.username }}</a>
</td>
{% else %}
<td>-</td>
{% endif %}
{% if g.user.is_superadmin %}
<td>{{ ban.ip_string }}</td>
{% else %}
<td>hidden</td>
{% endif %}
<td><div markdown-text-inline>{{ ban.reason }}</div></td>
<td class="center">{{ ban.created_time }}</td>
<td class="center">
{{ form.submit(label="Unban", value=ban.id, class="btn btn-danger btn-xs") }}
</td>
</tr>
{% endfor %}
</form>
</tbody>
</table>
</div>
<div class=pagination>
{% from "bootstrap/pagination.html" import render_pagination %}
{{ render_pagination(bans) }}
</div>
{% endblock %}

View file

@ -19,7 +19,11 @@
<td> <td>
<a href="{{ url_for('users.view_user', user_name=log.admin.username) }}">{{ log.admin.username }}</a> <a href="{{ url_for('users.view_user', user_name=log.admin.username) }}">{{ log.admin.username }}</a>
</td> </td>
<td><div markdown-text>{{ log.log }}</div></td> {% if g.user.is_superadmin %}
<td><div markdown-text-inline>{{ log.log }}</div></td>
{% else %}
<td><div markdown-text-inline>{{ log.log | regex_replace("IP\(.*?\)", "IP(hidden)" )}}</div></td>
{% endif %}
<td>{{ log.created_time }}</td> <td>{{ log.created_time }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -80,7 +80,7 @@
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<input type="submit" value="Save Changes" class="btn btn-primary"> {{ form.submit(class="btn btn-primary") }}
</div> </div>
</div> </div>
</form> </form>
@ -93,10 +93,22 @@
<h3 class="panel-title">Danger Zone</h3> <h3 class="panel-title">Danger Zone</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form method="POST" action="{{ url_for('torrents.delete', torrent_id=torrent.id) }}"> <form method="POST">
{{ delete_form.csrf_token }} {{ delete_form.csrf_token }}
{% if not torrent.deleted %} <p>
{% if torrent.deleted %}
This torrent is <strong>deleted</strong>{% if torrent.banned %} and <strong>banned</strong>{% endif %}.<br>
{% endif %}
{% if torrent.user and torrent.user.is_banned %}
The uploader is <strong>banned</strong>.<br>
{% endif %}
{% if ipbanned %}
The uploader is <strong>IP banned</strong>.<br>
{% endif %}
</p>
{% if not torrent.deleted %}
<p class="lead"> <p class="lead">
Delete torrent. Delete torrent.
{{ delete_form.delete(class="btn btn-danger pull-right") }} {{ delete_form.delete(class="btn btn-danger pull-right") }}
@ -113,11 +125,11 @@
{{ delete_form.ban(class="btn btn-danger pull-right") }} {{ delete_form.ban(class="btn btn-danger pull-right") }}
</p> </p>
<p> <p>
Soft deletes the torrent and disallows reuploading it. Soft deletes the torrent.<br>
Bans it from the tracker and disallows reuploading it.
</p> </p>
{% endif %} {% endif %}
{% else %} {% else %} {# if torrent.deleted #}
<p>This torrent is <strong>deleted</strong>{% if torrent.banned %} and <strong>banned</strong>{% endif %}.</p>
<p class="lead"> <p class="lead">
Undelete{% if torrent.banned %} and unban{% endif %} torrent. Undelete{% if torrent.banned %} and unban{% endif %} torrent.
{% if torrent.banned %} {% if torrent.banned %}
@ -140,19 +152,101 @@
Unbans torrent without undeleting it.<br> Unbans torrent without undeleting it.<br>
Allows reuploading this torrent again. Allows reuploading this torrent again.
</p> </p>
{% else%} {% else %}
<hr> <hr>
<p class="lead"> <p class="lead">
Ban torrent. Ban torrent.
{{ delete_form.ban(value="Ban", class="btn btn-danger pull-right") }} {{ delete_form.ban(value="Ban", class="btn btn-danger pull-right") }}
</p> </p>
<p> <p>
Bans the torrent.<br> Bans it from the tracker and disallows reuploading it.
Disallows reuploading this torrent.
</p> </p>
{% endif %} {% endif %}
{% endif %}
{% if ban_form %}
{% if (torrent.user and not torrent.user.is_banned) or not ipbanned %}
<hr>
<p class="lead">
{% if torrent.deleted %}
{% if torrent.banned %}
Ban uploader.
{% else %}
Ban torrent and ban uploader.
{% endif %} {% endif %}
{% else %}
Delete and ban torrent and ban uploader.
{% endif %}
</p>
<p>
{% if torrent.deleted %}
{% if torrent.banned %}
Bans the uploader.
{% else %}
Bans it from the tracker and disallows reuploading it.<br>
Additionally bans the uploader.
{% endif %}
{% else %}
Soft deletes the torrent.<br>
Bans it from the tracker and disallows reuploading it.<br>
Additionally bans the uploader.
{% endif %}
</p>
{{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br>
{% if torrent.user %}
<div class="pull-left">
{% if torrent.user.is_banned %}
<button type="button" class="btn btn-danger disabled">Already banned</button>
{% 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 %}
</div>
<div class="pull-right">
{% if ipbanned %}
<button type="button" class="btn btn-danger disabled">Already IP banned</button>
{% 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 %}
</div>
{% else %}
<div class="pull-left">
<button type="button" class="btn btn-danger disabled">No user</button>
</div>
<div class="pull-right">
{% if ipbanned %}
<button type="button" class="btn btn-danger disabled">Already IP banned</button>
{% 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 %}
</div>
{% endif %}
{% endif %}
{% endif %}
</form> </form>
</div> </div>
</div> </div>

View file

@ -97,6 +97,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li {% if request.path == url_for('admin.reports') %}class="active"{% endif %}><a href="{{ url_for('admin.reports') }}">Reports</a></li> <li {% if request.path == url_for('admin.reports') %}class="active"{% endif %}><a href="{{ url_for('admin.reports') }}">Reports</a></li>
<li {% if request.path == url_for('admin.log') %}class="active"{% endif %}><a href="{{ url_for('admin.log') }}">Log</a></li> <li {% if request.path == url_for('admin.log') %}class="active"{% endif %}><a href="{{ url_for('admin.log') }}">Log</a></li>
<li {% if request.path == url_for('admin.bans') %}class="active"{% endif %}><a href="{{ url_for('admin.bans') }}">Bans</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View file

@ -11,14 +11,15 @@
{% block body %} {% block body %}
{% from "_formhelpers.html" import render_menu_with_button %} {% from "_formhelpers.html" import render_menu_with_button %}
{% from "_formhelpers.html" import render_field %}
{% if g.user and g.user.is_moderator %} {% if g.user and g.user.is_moderator %}
<h2>User Information</h2><br> <h2>User Information</h2><br>
<div class="row" style="margin-bottom: 20px;"> <div class="row" style="margin-bottom: 20px;">
<div class="col-sm-2" style="max-width: 150px;"> <div class="col-md-2" style="max-width: 150px;">
<img class="avatar" src="{{ user.gravatar_url() }}"> <img class="avatar" src="{{ user.gravatar_url() }}">
</div> </div>
<div class="col-sm-10"> <div class="col-md-4">
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>User ID:</dt> <dt>User ID:</dt>
<dd>{{ user.id }}</dd> <dd>{{ user.id }}</dd>
@ -35,20 +36,74 @@
<dd>{{ user.ip_string }}</dd><br> <dd>{{ user.ip_string }}</dd><br>
{%- endif -%} {%- endif -%}
</dl> </dl>
</div>
</div>
{% if admin_form %} {% if admin_form %}
<form method="POST"> <form method="POST">
{{ admin_form.csrf_token }} {{ admin_form.csrf_token }}
<div class="form-group row"> <div class="form-group">
<div class="col-md-6">
{{ render_menu_with_button(admin_form.user_class) }} {{ render_menu_with_button(admin_form.user_class) }}
</div> </div>
</div>
</form> </form>
<br> <br>
{% endif %} {% endif %}
</div>
{% if ban_form %}
<div class="col-md-5">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">Danger Zone</h3>
</div>
<div class="panel-body">
<form method="POST">
{{ ban_form.csrf_token }}
{% if user.is_banned %}
This user is <strong>banned</strong>.<br>
{% endif %}
{% if ipbanned %}
This user is <strong>ip banned</strong>.<br>
{% endif %}
{% if not user.is_banned and not bans %}
This user is <strong>not banned</strong>.<br>
{% endif %}
<br>
{% if user.is_banned or bans %}
<p>
{% for ban in bans %}
#{{ ban.id }}
by <a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a>
for <span markdown-text-inline>{{ ban.reason }}</span>
<br>
{% endfor %}
</p>
{{ ban_form.unban(class="btn btn-info") }}
{% endif %}
{% if not user.is_banned or not ipbanned %}
{% if user.is_banned or bans %}
<hr>
{% endif %}
{{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br>
<div class="pull-left">
{% if not user.is_banned %}
{{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }}
{% else %}
<button type="button" class="btn btn-danger disabled">Already banned</button>
{% endif %}
</div>
<div class="pull-right">
{% if not ipbanned %}
{{ ban_form.ban_userip(value="Ban User+IP", class="btn btn-danger") }}
{% else %}
<button type="button" class="btn btn-danger disabled">Already IP banned</button>
{% endif %}
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endif %}
</div>
{% endif %} {% endif %}
<h3> <h3>

View file

@ -28,12 +28,16 @@ def login():
if not user: if not user:
user = models.User.by_email(username) user = models.User.by_email(username)
if (not user or password != user.password_hash or if not user or password != user.password_hash:
user.status == models.UserStatusType.INACTIVE):
flask.flash(flask.Markup( flask.flash(flask.Markup(
'<strong>Login failed!</strong> Incorrect username or password.'), 'danger') '<strong>Login failed!</strong> Incorrect username or password.'), 'danger')
return flask.redirect(flask.url_for('account.login')) return flask.redirect(flask.url_for('account.login'))
if user.status != models.UserStatusType.ACTIVE:
flask.flash(flask.Markup(
'<strong>Login failed!</strong> Account is not activated or banned.'), 'danger')
return flask.redirect(flask.url_for('account.login'))
user.last_login_date = datetime.utcnow() user.last_login_date = datetime.utcnow()
user.last_login_ip = ip_address(flask.request.remote_addr).packed user.last_login_ip = ip_address(flask.request.remote_addr).packed
db.session.add(user) db.session.add(user)

View file

@ -1,3 +1,5 @@
from ipaddress import ip_address
import flask import flask
from nyaa import forms, models from nyaa import forms, models
@ -20,6 +22,45 @@ def view_adminlog():
adminlog=logs) 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']) @bp.route('/reports', endpoint='reports', methods=['GET', 'POST'])
def view_reports(): def view_reports():
if not flask.g.user or not flask.g.user.is_moderator: 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: if not torrent or not report or report.status != 0:
flask.abort(404) flask.abort(404)
else: else:
log = "Report #{}: {} [#{}]({}), reported by [{}]({})" log = 'Report #{}: {} [#{}]({}), reported by [{}]({})'
if action == 'delete': if action == 'delete':
torrent.deleted = True torrent.deleted = True
report.status = 1 report.status = 1

View file

@ -1,6 +1,7 @@
import math import math
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ipaddress import ip_address
import flask import flask
from flask_paginate import Pagination from flask_paginate import Pagination
@ -28,6 +29,10 @@ def before_request():
if not user: if not user:
return logout() return logout()
# Logout inactive and banned users
if user.status != models.UserStatusType.ACTIVE:
return logout()
flask.g.user = user flask.g.user = user
if 'timeout' not in flask.session or flask.session['timeout'] < datetime.now(): 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.permanent = True
flask.session.modified = 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 return 'You are banned.', 403

View file

@ -1,5 +1,6 @@
import json import json
import os.path import os.path
from ipaddress import ip_address
from urllib.parse import quote from urllib.parse import quote
import flask import flask
@ -80,8 +81,9 @@ def view_torrent(torrent_id):
def edit_torrent(torrent_id): def edit_torrent(torrent_id):
torrent = models.Torrent.by_id(torrent_id) torrent = models.Torrent.by_id(torrent_id)
form = forms.EditForm(flask.request.form) form = forms.EditForm(flask.request.form)
delete_form = forms.DeleteForm()
form.category.choices = _create_upload_category_choices() form.category.choices = _create_upload_category_choices()
delete_form = forms.DeleteForm()
ban_form = None
editor = flask.g.user 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): if not editor or not (editor is torrent.user or editor.is_moderator):
flask.abort(403) 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. # Form has been sent, edit torrent with data.
torrent.main_category_id, torrent.sub_category_id = \ torrent.main_category_id, torrent.sub_category_id = \
form.category.parsed_data.get_category_ids() form.category.parsed_data.get_category_ids()
@ -127,9 +132,12 @@ def edit_torrent(torrent_id):
flask.flash(flask.Markup( flask.flash(flask.Markup(
'Torrent has been successfully edited! Changes might take a few minutes to show up.'), 'Torrent has been successfully edited! Changes might take a few minutes to show up.'),
'info') 'success')
return flask.redirect(url) 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: else:
if flask.request.method != 'POST': if flask.request.method != 'POST':
# Fill form data only if the POST didn't fail # 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_trusted.data = torrent.trusted
form.is_deleted.data = torrent.deleted 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', return flask.render_template('edit.html',
form=form, form=form,
delete_form=delete_form, delete_form=delete_form,
torrent=torrent) ban_form=ban_form,
torrent=torrent,
ipbanned=ipbanned)
@bp.route('/view/<int:torrent_id>/delete', endpoint='delete', methods=['POST']) def _delete_torrent(torrent, form, banform):
def delete_torrent(torrent_id):
torrent = models.Torrent.by_id(torrent_id)
form = forms.DeleteForm(flask.request.form)
editor = flask.g.user editor = flask.g.user
uploader = torrent.user
if not torrent:
flask.abort(404)
# Only allow admins edit deleted torrents # Only allow admins edit deleted torrents
if torrent.deleted and not (editor and editor.is_moderator): if torrent.deleted and not (editor and editor.is_moderator):
flask.abort(404) 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 action = None
url = flask.url_for('main.home') 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: if form.delete.data and not torrent.deleted:
action = 'deleted' action = 'deleted'
torrent.deleted = True torrent.deleted = True
db.session.add(torrent) 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' action = 'banned'
torrent.banned = True torrent.banned = True
if not torrent.deleted: if not torrent.deleted:
@ -202,20 +214,80 @@ def delete_torrent(torrent_id):
backend.tracker_api([torrent.info_hash], 'unban') backend.tracker_api([torrent.info_hash], 'unban')
db.session.add(torrent) 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') flask.flash(flask.Markup('What the fuck are you doing?'), 'danger')
return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) 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) 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) log = "Torrent [#{0}]({1}) has been {2}".format(torrent.id, url, action)
adminlog = models.AdminLog(log=log, admin_id=editor.id) adminlog = models.AdminLog(log=log, admin_id=editor.id)
db.session.add(adminlog) 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() 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) return flask.redirect(url)

View file

@ -1,4 +1,5 @@
import math import math
from ipaddress import ip_address
import flask import flask
from flask_paginate import Pagination from flask_paginate import Pagination
@ -23,14 +24,23 @@ def view_user(user_name):
flask.abort(404) flask.abort(404)
admin_form = None 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: if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level:
admin_form = forms.UserForm() admin_form = forms.UserForm()
default, admin_form.user_class.choices = _create_user_class_choices(user) default, admin_form.user_class.choices = _create_user_class_choices(user)
if flask.request.method == 'GET': if flask.request.method == 'GET':
admin_form.user_class.data = default 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) 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 selection = admin_form.user_class.data
log = None log = None
if selection == 'regular': if selection == 'regular':
@ -42,9 +52,6 @@ def view_user(user_name):
elif selection == 'moderator': elif selection == 'moderator':
user.level = models.UserLevelType.MODERATOR user.level = models.UserLevelType.MODERATOR
log = "[{}]({}) changed to moderator user".format(user_name, url) 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) adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id)
db.session.add(user) db.session.add(user)
@ -53,6 +60,46 @@ def view_user(user_name):
return flask.redirect(url) 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 req_args = flask.request.args
search_term = chain_get(req_args, 'q', 'term') search_term = chain_get(req_args, 'q', 'term')
@ -117,7 +164,10 @@ def view_user(user_name):
user=user, user=user,
user_page=True, user_page=True,
rss_filter=rss_query_string, 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 # Similar logic as home page
else: else:
if use_elastic: if use_elastic:
@ -132,7 +182,10 @@ def view_user(user_name):
user=user, user=user,
user_page=True, user_page=True,
rss_filter=rss_query_string, 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/<payload>') @bp.route('/user/activate/<payload>')
@ -164,8 +217,6 @@ def _create_user_class_choices(user):
choices.append(('trusted', 'Trusted')) choices.append(('trusted', 'Trusted'))
if flask.g.user.is_superadmin: if flask.g.user.is_superadmin:
choices.append(('moderator', 'Moderator')) choices.append(('moderator', 'Moderator'))
if flask.g.user.is_moderator:
choices.append(('banned', 'Banned'))
if user: if user:
if user.is_moderator: if user.is_moderator: