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

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 app, db
from nyaa import models, forms from nyaa import models, forms
from nyaa import bencode, utils from nyaa import bencode, utils
@ -8,6 +9,7 @@ import json
from werkzeug import secure_filename from werkzeug import secure_filename
from collections import OrderedDict from collections import OrderedDict
from orderedset import OrderedSet from orderedset import OrderedSet
from ipaddress import ip_address
def _replace_utf8_values(dict_or_list): def _replace_utf8_values(dict_or_list):
@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
description=description, description=description,
encoding=torrent_encoding, encoding=torrent_encoding,
filesize=torrent_filesize, filesize=torrent_filesize,
user=uploading_user) user=uploading_user,
uploader_ip=ip_address(flask.request.remote_addr).packed)
# Store bencoded info_dict # Store bencoded info_dict
torrent.info = models.TorrentInfo(info_dict=torrent_data.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): class UserForm(FlaskForm):
user_class = DisabledSelectField('Change User Class') user_class = SelectField('Change User Class')
def validate_user_class(form, field): def validate_user_class(form, field):
if not field.data: if not field.data:
@ -294,7 +294,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None):
for announce in announce_list: for announce in announce_list:
_validate_list(announce, 'announce-list item') _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(): if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower():
tracker_found = True tracker_found = True

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,10 @@
{%- if not torrent.anonymous and torrent.user -%} {%- if not torrent.anonymous and torrent.user -%}
<a href="{{ user_url }}">{{ torrent.user.username }}</a> <a href="{{ user_url }}">{{ torrent.user.username }}</a>
{%- else -%} {%- 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 -%} {%- endif -%}
</div> </div>