Compare commits

..

7 Commits

Author SHA1 Message Date
rubenwardy
a36e233051 Fix API auth crash and add more error messages 2020-05-19 17:24:57 +01:00
rubenwardy
8484c0f0aa Fix minor security vulnerability 2020-05-19 16:46:47 +01:00
rubenwardy
ffb5b49521 Fix crash on invalid protocol_version 2020-05-19 16:39:39 +01:00
rubenwardy
c15dd183a0 Update top packages 2020-04-30 22:41:55 +01:00
rubenwardy
0eca2d49ba Add celery exporter 2020-04-24 00:49:40 +01:00
rubenwardy
57e7cbfd09 Make OpenGraph URLs absolute 2020-04-23 23:51:10 +01:00
Lars Mueller
e94bd9b845 Add meta to package view pages 2020-04-23 23:51:03 +01:00
10 changed files with 73 additions and 25 deletions

View File

@@ -16,6 +16,7 @@
from flask import request, make_response, jsonify, abort from flask import request, make_response, jsonify, abort
from app.models import APIToken from app.models import APIToken
from .support import error
from functools import wraps from functools import wraps
def is_api_authd(f): def is_api_authd(f):
@@ -29,13 +30,13 @@ def is_api_authd(f):
elif value[0:7].lower() == "bearer ": elif value[0:7].lower() == "bearer ":
access_token = value[7:] access_token = value[7:]
if len(access_token) < 10: if len(access_token) < 10:
abort(400) error(400, "API token is too short")
token = APIToken.query.filter_by(access_token=access_token).first() token = APIToken.query.filter_by(access_token=access_token).first()
if token is None: if token is None:
abort(403) error(403, "Unknown API token")
else: else:
abort(403) abort(403, "Unsupported authentication method")
return f(token=token, *args, **kwargs) return f(token=token, *args, **kwargs)

View File

@@ -143,19 +143,21 @@ def markdown():
@is_package_page @is_package_page
@is_api_authd @is_api_authd
def create_release(token, package): def create_release(token, package):
if not token:
error(401, "Authentication needed")
if not package.checkPerm(token.owner, Permission.APPROVE_RELEASE): if not package.checkPerm(token.owner, Permission.APPROVE_RELEASE):
return error(403, "You do not have the permission to approve releases") error(403, "You do not have the permission to approve releases")
json = request.json json = request.json
if json is None: if json is None:
return error(400, "JSON post data is required") error(400, "JSON post data is required")
for option in ["method", "title", "ref"]: for option in ["method", "title", "ref"]:
if json.get(option) is None: if json.get(option) is None:
return error(400, option + " is required in the POST data") error(400, option + " is required in the POST data")
if json["method"].lower() != "git": if json["method"].lower() != "git":
return error(400, "Release-creation methods other than git are not supported") error(400, "Release-creation methods other than git are not supported")
return handleCreateRelease(token, package, json["title"], json["ref"]) return handleCreateRelease(token, package, json["title"], json["ref"])

View File

@@ -1,12 +1,12 @@
from app.models import PackageRelease, db, Permission from app.models import PackageRelease, db, Permission
from app.tasks.importtasks import makeVCSRelease from app.tasks.importtasks import makeVCSRelease
from celery import uuid from celery import uuid
from flask import jsonify, make_response, url_for from flask import jsonify, abort, url_for
import datetime import datetime
def error(status, message): def error(status, message):
return make_response(jsonify({ "success": False, "error": message }), status) abort(status, jsonify({ "success": False, "error": message }))
def handleCreateRelease(token, package, title, ref): def handleCreateRelease(token, package, title, ref):

View File

@@ -9,6 +9,8 @@ Authentication is done using Bearer tokens:
You can use the `/api/whoami` to check authentication. You can use the `/api/whoami` to check authentication.
Tokens can be attained by visiting "API Tokens" on your profile page.
## Endpoints ## Endpoints
### Misc ### Misc
@@ -16,7 +18,7 @@ You can use the `/api/whoami` to check authentication.
* GET `/api/whoami/` - Json dictionary with the following keys: * GET `/api/whoami/` - Json dictionary with the following keys:
* `is_authenticated` - True on successful API authentication * `is_authenticated` - True on successful API authentication
* `username` - Username of the user authenticated as, null otherwise. * `username` - Username of the user authenticated as, null otherwise.
* 403 will be thrown on unsupported authentication type, invalid access token, or other errors. * 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
### Packages ### Packages

View File

@@ -1,14 +1,18 @@
title: Top Packages Algorithm title: Top Packages Algorithm
## Pseudo rolling average ## Score
A package's score is currently equal to a pseudo rolling average of downloads.
In the future, a package will also gain score through reviews.
## Pseudo rolling average of downloads
Every package loses 5% of its score every day. Every package loses 5% of its score every day.
An open source package will gain 1 score for each unique download, An open source package will gain 1 score for each unique download,
whereas a non-free package will only gain 0.1 score. whereas a non-free package will only gain 0.1 score.
A package currently only gains score through downloads. This metric aims to be roughly equivalent to the average downloads.
In the future, a package will also gain score through reviews.
## Seeded using a legacy heuristic ## Seeded using a legacy heuristic
@@ -32,8 +36,14 @@ As said, this legacy score is no longer used when ranking mods.
It was only used to provide an initial score for the rolling average, It was only used to provide an initial score for the rolling average,
which was 20% of the above value. which was 20% of the above value.
## Manual adjustments
The admin occasionally reduces all packages by a set percentage to speed up
convergence. Convergence is when
## Transparency and Feedback ## Transparency and Feedback
You can see all scores using the [scores REST API](/api/scores/). You can see all scores using the [scores REST API](/api/scores/), or by
using the [Prometheus metrics](/help/metrics/) endpoint.
Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=). Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).

View File

@@ -578,13 +578,25 @@ class Package(db.Model):
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first() screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.getThumbnailURL(level) if screenshot is not None else None return screenshot.getThumbnailURL(level) if screenshot is not None else None
def getMainScreenshotURL(self): def getMainScreenshotURL(self, absolute=False):
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first() screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.url if screenshot is not None else None if screenshot is None:
return None
def getDetailsURL(self): if absolute:
return url_for("packages.view", from app.utils import abs_url
author=self.author.username, name=self.name) return abs_url(screenshot.url)
else:
return screenshot.url
def getDetailsURL(self, absolute=False):
if absolute:
from app.utils import abs_url_for
return abs_url_for("packages.view",
author=self.author.username, name=self.name)
else:
return url_for("packages.view",
author=self.author.username, name=self.name)
def getEditURL(self): def getEditURL(self):
return url_for("packages.create_edit", return url_for("packages.create_edit",
@@ -835,12 +847,11 @@ class PackageRelease(db.Model):
name=self.package.name, name=self.package.name,
id=self.id) id=self.id)
def __init__(self): def __init__(self):
self.releaseDate = datetime.datetime.now() self.releaseDate = datetime.datetime.now()
def approve(self, user): def approve(self, user):
if self.package.approved and \ if self.package.approved or \
not self.package.checkPerm(user, Permission.APPROVE_RELEASE): not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
return False return False

View File

@@ -1,6 +1,6 @@
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag
from .models import tags as Tags from .models import tags as Tags
from .utils import isNo, isYes from .utils import isNo, isYes, get_int_or_abort
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from flask import abort from flask import abort
from sqlalchemy import or_ from sqlalchemy import or_
@@ -61,7 +61,7 @@ class QueryBuilder:
if not self.protocol_version: if not self.protocol_version:
return None return None
self.protocol_version = int(self.protocol_version) self.protocol_version = get_int_or_abort(self.protocol_version)
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first() version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
if version is not None: if version is not None:
return version.id return version.id
@@ -139,7 +139,6 @@ class QueryBuilder:
query = query.order_by(db.desc(ForumTopic.views)) query = query.order_by(db.desc(ForumTopic.views))
elif self.order_by == "date": elif self.order_by == "date":
query = query.order_by(db.asc(ForumTopic.created_at)) query = query.order_by(db.asc(ForumTopic.created_at))
sort_by = "date"
if self.search: if self.search:
query = query.filter(ForumTopic.title.ilike('%' + self.search + '%')) query = query.filter(ForumTopic.title.ilike('%' + self.search + '%'))

View File

@@ -6,6 +6,16 @@
{{ package.title }} {{ package.title }}
{% endblock %} {% endblock %}
{% block headextra %}
<meta name="og:title" content="{{ package.title }}"/>
<meta name="og:description" content="{{ package.short_desc }}"/>
<meta name="description" content="{{ package.short_desc }}"/>
<meta name="og:url" content="{{ package.getDetailsURL(absolute=True) }}"/>
{% if package.getMainScreenshotURL() %}
<meta name="og:image" content="{{ package.getMainScreenshotURL(absolute=True) }}"/>
{% endif %}
{% endblock %}
{% block container %} {% block container %}
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %} {% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %}
{% set package_warning="Non-free code and media" %} {% set package_warning="Non-free code and media" %}

View File

@@ -21,11 +21,15 @@ from flask_login import login_user, logout_user
from .models import * from .models import *
from . import app from . import app
import random, string, os, imghdr import random, string, os, imghdr
from urllib.parse import urljoin
def abs_url_for(path, **kwargs): def abs_url_for(path, **kwargs):
scheme = "https" if app.config["BASE_URL"][:5] == "https" else "http" scheme = "https" if app.config["BASE_URL"][:5] == "https" else "http"
return url_for(path, _external=True, _scheme=scheme, **kwargs) return url_for(path, _external=True, _scheme=scheme, **kwargs)
def abs_url(path):
return urljoin(app.config["BASE_URL"], path)
def get_int_or_abort(v, default=None): def get_int_or_abort(v, default=None):
try: try:
return int(v or default) return int(v or default)

View File

@@ -60,3 +60,12 @@ services:
- 5124:5124 - 5124:5124
depends_on: depends_on:
- redis - redis
exporter:
image: ovalmoney/celery-exporter
env_file:
- config.env
ports:
- 5125:9540
depends_on:
- redis