DB CHANGE: Add uploader ip address to torrent column and show on torrent view page for superadmins.

Added migration script!: remove sukebei_ lines if your local db does not have those.
Show users ip address on user page for superadmins.
Rename Admin to Moderator internally.
Moderators can now change user level to trusted.
Superadmins can make users moderator.
Improve changing user level.
This commit is contained in:
nyaadev 2017-05-21 19:12:15 +02:00
parent 152e547ac5
commit 9af778217b
8 changed files with 119 additions and 58 deletions

View File

@ -0,0 +1,30 @@
"""Add uploader_ip column to torrents table.
Revision ID: 3001f79b7722
Revises:
Create Date: 2017-05-21 18:01:35.472717
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3001f79b7722'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('nyaa_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True))
op.add_column('sukebei_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('nyaa_torrents', 'uploader_ip')
op.drop_column('sukebei_torrents', 'uploader_ip')
# ### end Alembic commands ###

View File

@ -1,3 +1,4 @@
import flask
from nyaa import app, db
from nyaa import models, forms
from nyaa import bencode, utils
@ -8,6 +9,7 @@ import json
from werkzeug import secure_filename
from collections import OrderedDict
from orderedset import OrderedSet
from ipaddress import ip_address
def _replace_utf8_values(dict_or_list):
@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
description=description,
encoding=torrent_encoding,
filesize=torrent_filesize,
user=uploading_user)
user=uploading_user,
uploader_ip=ip_address(flask.request.remote_addr).packed)
# Store bencoded info_dict
torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict)

View File

@ -263,7 +263,7 @@ class UploadForm(FlaskForm):
class UserForm(FlaskForm):
user_class = DisabledSelectField('Change User Class')
user_class = SelectField('Change User Class')
def validate_user_class(form, field):
if not field.data:
@ -294,7 +294,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None):
for announce in announce_list:
_validate_list(announce, 'announce-list item')
announce_string = _validate_bytes(announce[0], 'announce-list item url', test_decode='utf-8')
announce_string = _validate_bytes(
announce[0], 'announce-list item url', test_decode='utf-8')
if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower():
tracker_found = True

View File

@ -6,6 +6,7 @@ from sqlalchemy import func, ForeignKeyConstraint, Index
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy_fulltext import FullText
from ipaddress import ip_address
import re
import base64
@ -61,6 +62,7 @@ class Torrent(db.Model):
encoding = db.Column(db.String(length=32), nullable=False)
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
uploader_ip = db.Column(db.Binary(length=16), default=None, nullable=True)
has_torrent = db.Column(db.Boolean, nullable=False, default=False)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
@ -92,11 +94,11 @@ class Torrent(db.Model):
info = db.relationship('TorrentInfo', uselist=False,
cascade="all, delete-orphan", back_populates='torrent')
filelist = db.relationship('TorrentFilelist', uselist=False,
cascade="all, delete-orphan", back_populates='torrent')
cascade="all, delete-orphan", back_populates='torrent')
stats = db.relationship('Statistic', uselist=False,
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
trackers = db.relationship('TorrentTrackers', uselist=True,
cascade="all, delete-orphan", lazy='joined')
cascade="all, delete-orphan", lazy='joined')
def __repr__(self):
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
@ -138,6 +140,11 @@ class Torrent(db.Model):
def magnet_uri(self):
return create_magnet(self)
@property
def uploader_ip_string(self):
if self.uploader_ip:
return str(ip_address(self.uploader_ip))
@property
def anonymous(self):
return self.flags & TorrentFlags.ANONYMOUS
@ -313,7 +320,7 @@ class SubCategory(db.Model):
class UserLevelType(IntEnum):
REGULAR = 0
TRUSTED = 1
ADMIN = 2
MODERATOR = 2
SUPERADMIN = 3
@ -362,6 +369,11 @@ class User(db.Model):
]
return all(checks)
@property
def ip_string(self):
if self.last_login_ip:
return str(ip_address(self.last_login_ip))
@classmethod
def by_id(cls, id):
return cls.query.get(id)
@ -381,8 +393,8 @@ class User(db.Model):
return cls.by_username(username_or_email) or cls.by_email(username_or_email)
@property
def is_admin(self):
return self.level >= UserLevelType.ADMIN
def is_moderator(self):
return self.level >= UserLevelType.MODERATOR
@property
def is_superadmin(self):

View File

@ -11,7 +11,7 @@ import config
import json
from datetime import datetime, timedelta
import ipaddress
from ipaddress import ip_address
import os.path
import base64
from urllib.parse import quote
@ -135,6 +135,7 @@ def get_category_id_map():
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
def chain_get(source, *args):
''' Tries to return values from source by the given keys.
Returns None if none match.
@ -146,6 +147,7 @@ def chain_get(source, *args):
return value
return None
@app.route('/rss', defaults={'rss': True})
@app.route('/', defaults={'rss': False})
def home(rss):
@ -194,7 +196,7 @@ def home(rss):
if flask.g.user:
query_args['logged_in_user'] = flask.g.user
if flask.g.user.is_admin: # God mode
if flask.g.user.is_moderator: # God mode
query_args['admin'] = True
# If searching, we get results from elastic search
@ -215,7 +217,8 @@ def home(rss):
if render_as_rss:
return render_rss('"{}"'.format(search_term), query_results, use_elastic=True, magnet_links=use_magnet_links)
else:
rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name)
rss_query_string = _generate_query_string(
search_term, category, quality_filter, user_name)
max_results = min(max_search_results, query_results['hits']['total'])
# change p= argument to whatever you change page_parameter to or pagination breaks
pagination = Pagination(p=query_args['page'], per_page=results_per_page,
@ -238,7 +241,8 @@ def home(rss):
if render_as_rss:
return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links)
else:
rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name)
rss_query_string = _generate_query_string(
search_term, category, quality_filter, user_name)
# Use elastic is always false here because we only hit this section
# if we're browsing without a search term (which means we default to DB)
# or if ES is disabled
@ -256,22 +260,23 @@ def view_user(user_name):
if not user:
flask.abort(404)
if flask.g.user and flask.g.user.id != user.id:
admin = flask.g.user.is_admin
superadmin = flask.g.user.is_superadmin
else:
admin = False
superadmin = False
admin_form = 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
form = forms.UserForm()
form.user_class.choices = _create_user_class_choices()
if flask.request.method == 'POST' and form.validate():
selection = form.user_class.data
if flask.request.method == 'POST' and admin_form and admin_form.validate():
selection = admin_form.user_class.data
if selection == 'regular':
user.level = models.UserLevelType.REGULAR
elif selection == 'trusted':
user.level = models.UserLevelType.TRUSTED
elif selection == 'moderator':
user.level = models.UserLevelType.MODERATOR
db.session.add(user)
db.session.commit()
@ -311,7 +316,7 @@ def view_user(user_name):
if flask.g.user:
query_args['logged_in_user'] = flask.g.user
if flask.g.user.is_admin: # God mode
if flask.g.user.is_moderator: # God mode
query_args['admin'] = True
# Use elastic search for term searching
@ -344,9 +349,7 @@ def view_user(user_name):
user_page=True,
rss_filter=rss_query_string,
level=user_level,
admin=admin,
superadmin=superadmin,
form=form)
admin_form=admin_form)
# Similar logic as home page
else:
if use_elastic:
@ -362,9 +365,7 @@ def view_user(user_name):
user_page=True,
rss_filter=rss_query_string,
level=user_level,
admin=admin,
superadmin=superadmin,
form=form)
admin_form=admin_form)
@app.template_filter('rfc822')
@ -417,7 +418,7 @@ def login():
return flask.redirect(flask.url_for('login'))
user.last_login_date = datetime.utcnow()
user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed
user.last_login_ip = ip_address(flask.request.remote_addr).packed
db.session.add(user)
db.session.commit()
@ -451,7 +452,7 @@ def register():
if flask.request.method == 'POST' and form.validate():
user = models.User(username=form.username.data.strip(),
email=form.email.data.strip(), password=form.password.data)
user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed
user.last_login_ip = ip_address(flask.request.remote_addr).packed
db.session.add(user)
db.session.commit()
@ -479,13 +480,7 @@ def profile():
form = forms.ProfileForm(flask.request.form)
level = 'Regular'
if flask.g.user.is_admin:
level = 'Moderator'
if flask.g.user.is_superadmin: # check this second because we can be admin AND superadmin
level = 'Administrator'
elif flask.g.user.is_trusted:
level = 'Trusted'
level = ['Regular', 'Trusted', 'Moderator', 'Administrator'][flask.g.user.level]
if flask.request.method == 'POST' and form.validate():
user = flask.g.user
@ -586,11 +581,11 @@ def view_torrent(torrent_id):
flask.abort(404)
# Only allow admins see deleted torrents
if torrent.deleted and not (viewer and viewer.is_admin):
if torrent.deleted and not (viewer and viewer.is_moderator):
flask.abort(404)
# Only allow owners and admins to edit torrents
can_edit = viewer and (viewer is torrent.user or viewer.is_admin)
can_edit = viewer and (viewer is torrent.user or viewer.is_moderator)
files = None
if torrent.filelist:
@ -614,11 +609,11 @@ def edit_torrent(torrent_id):
flask.abort(404)
# Only allow admins edit deleted torrents
if torrent.deleted and not (editor and editor.is_admin):
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_admin):
if not editor or not (editor is torrent.user or editor.is_moderator):
flask.abort(403)
if flask.request.method == 'POST' and form.validate():
@ -636,7 +631,7 @@ def edit_torrent(torrent_id):
if editor.is_trusted:
torrent.trusted = form.is_trusted.data
if editor.is_admin:
if editor.is_moderator:
torrent.deleted = form.is_deleted.data
db.session.commit()
@ -739,11 +734,22 @@ def send_verification_email(to_address, activ_link):
server.quit()
def _create_user_class_choices():
def _create_user_class_choices(user):
choices = [('regular', 'Regular')]
if flask.g.user and flask.g.user.is_superadmin:
choices.append(('trusted', 'Trusted'))
return choices
default = 'regular'
if flask.g.user:
if flask.g.user.is_moderator:
choices.append(('trusted', 'Trusted'))
if flask.g.user.is_superadmin:
choices.append(('moderator', 'Moderator'))
if user:
if user.is_moderator:
default = 'moderator'
elif user.is_trusted:
default = 'trusted'
return default, choices
# #################################### STATIC PAGES ####################################

View File

@ -31,7 +31,7 @@
<div class="col-md-6">
<label class="control-label">Torrent flags</label>
<div>
{% if editor.is_admin %}
{% if editor.is_moderator %}
<label class="btn btn-primary">
{{ form.is_deleted }}
Deleted

View File

@ -3,32 +3,38 @@
{% block body %}
{% from "_formhelpers.html" import render_menu_with_button %}
{% if superadmin %}
{% if g.user and g.user.is_moderator %}
<h2>User Information</h2><br>
<dl class="dl-horizontal">
<dt>User ID:</dt>
<dd>{{user.id}}</dd>
<dd>{{ user.id }}</dd>
<dt>Account created on:</dt>
<dd>{{user.created_time}}</dd>
<dd>{{ user.created_time }}</dd>
<dt>Email address:</dt>
<dd>{{user.email}}</dd>
<dd>{{ user.email }}</dd>
<dt>User class:</dt>
<dd>{{level}}</dd><br>
<dd>{{ level }}</dd>
{%- if g.user.is_superadmin -%}
<dt>Last login IP:</dt>
<dd>{{ user.ip_string }}</dd><br>
{%- endif -%}
</dl>
{% if admin_form %}
<form method="POST">
{{ form.csrf_token }}
{{ admin_form.csrf_token }}
<div class="form-group row">
<div class="col-md-6">
{{ render_menu_with_button(form.user_class)}}
{{ render_menu_with_button(admin_form.user_class) }}
</div>
</div>
</form>
<br>
{% endif %}
{% endif %}
<h3>
Browsing {{user.username}}'s torrents
Browsing {{ user.username }}'s torrents
</h3>
{% include "search_results.html" %}

View File

@ -28,7 +28,10 @@
{%- if not torrent.anonymous and torrent.user -%}
<a href="{{ user_url }}">{{ torrent.user.username }}</a>
{%- else -%}
Anonymous {% if torrent.user and (viewer == torrent.user or viewer.is_admin) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %}
Anonymous {% if torrent.user and (viewer == torrent.user or viewer.is_moderator) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %}
{%- endif -%}
{%- if viewer and viewer.is_superadmin and torrent.uploader_ip -%}
({{ torrent.uploader_ip_string }})
{%- endif -%}
</div>