aboutsummaryrefslogtreecommitdiff
path: root/app/blueprints/api
diff options
context:
space:
mode:
authorrubenwardy <rw@rubenwardy.com>2019-11-22 14:33:22 +0000
committerrubenwardy <rw@rubenwardy.com>2019-11-27 01:06:58 +0000
commit4ce388c8aa5d5502408609983535a9812d41d6d1 (patch)
tree5ad9123949ca2068dfe975284d0f1b3acdf5b437 /app/blueprints/api
parentcb5451fe5d49e0eda379e3cd636c54e8ea1a3f8e (diff)
downloadcheatdb-4ce388c8aa5d5502408609983535a9812d41d6d1.tar.xz
Add API Token creation
Diffstat (limited to 'app/blueprints/api')
-rw-r--r--app/blueprints/api/__init__.py83
-rw-r--r--app/blueprints/api/auth.py42
-rw-r--r--app/blueprints/api/endpoints.py109
-rw-r--r--app/blueprints/api/tokens.py141
4 files changed, 294 insertions, 81 deletions
diff --git a/app/blueprints/api/__init__.py b/app/blueprints/api/__init__.py
index 5092f21..03adaf8 100644
--- a/app/blueprints/api/__init__.py
+++ b/app/blueprints/api/__init__.py
@@ -14,87 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-from flask import *
-from flask_user import *
-from app.models import *
-from app.utils import is_package_page
-from app.querybuilder import QueryBuilder
+from flask import Blueprint
bp = Blueprint("api", __name__)
-@bp.route("/api/packages/")
-def packages():
- qb = QueryBuilder(request.args)
- query = qb.buildPackageQuery()
- ver = qb.getMinetestVersion()
-
- pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
- for package in query.all()]
- return jsonify(pkgs)
-
-
-@bp.route("/api/packages/<author>/<name>/")
-@is_package_page
-def package(package):
- return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
-
-
-@bp.route("/api/packages/<author>/<name>/dependencies/")
-@is_package_page
-def package_dependencies(package):
- ret = []
-
- for dep in package.dependencies:
- name = None
- fulfilled_by = None
-
- if dep.package:
- name = dep.package.name
- fulfilled_by = [ dep.package.getAsDictionaryKey() ]
-
- elif dep.meta_package:
- name = dep.meta_package.name
- fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
-
- else:
- raise "Malformed dependency"
-
- ret.append({
- "name": name,
- "is_optional": dep.optional,
- "packages": fulfilled_by
- })
-
- return jsonify(ret)
-
-
-@bp.route("/api/topics/")
-def topics():
- qb = QueryBuilder(request.args)
- query = qb.buildTopicQuery(show_added=True)
- return jsonify([t.getAsDictionary() for t in query.all()])
-
-
-@bp.route("/api/topic_discard/", methods=["POST"])
-@login_required
-def topic_set_discard():
- tid = request.args.get("tid")
- discard = request.args.get("discard")
- if tid is None or discard is None:
- abort(400)
-
- topic = ForumTopic.query.get(tid)
- if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
- abort(403)
-
- topic.discarded = discard == "true"
- db.session.commit()
-
- return jsonify(topic.getAsDictionary())
-
-
-@bp.route("/api/minetest_versions/")
-def versions():
- return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
- for rel in MinetestRelease.query.all() if rel.getActual() is not None])
+from . import tokens, endpoints
diff --git a/app/blueprints/api/auth.py b/app/blueprints/api/auth.py
new file mode 100644
index 0000000..6eeadde
--- /dev/null
+++ b/app/blueprints/api/auth.py
@@ -0,0 +1,42 @@
+# Content DB
+# Copyright (C) 2019 rubenwardy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from flask import request, make_response, jsonify, abort
+from app.models import APIToken
+from functools import wraps
+
+def is_api_authd(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ token = None
+
+ value = request.headers.get("authorization")
+ if value is None:
+ pass
+ elif value[0:7].lower() == "bearer ":
+ access_token = value[7:]
+ if len(access_token) < 10:
+ abort(400)
+
+ token = APIToken.query.filter_by(access_token=access_token).first()
+ if token is None:
+ abort(403)
+ else:
+ abort(403)
+
+ return f(token=token, *args, **kwargs)
+
+ return decorated_function
diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py
new file mode 100644
index 0000000..e37454f
--- /dev/null
+++ b/app/blueprints/api/endpoints.py
@@ -0,0 +1,109 @@
+# Content DB
+# Copyright (C) 2018 rubenwardy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from . import bp
+from .auth import is_api_authd
+from app.models import *
+from app.utils import is_package_page
+from app.querybuilder import QueryBuilder
+
+@bp.route("/api/packages/")
+def packages():
+ qb = QueryBuilder(request.args)
+ query = qb.buildPackageQuery()
+ ver = qb.getMinetestVersion()
+
+ pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
+ for package in query.all()]
+ return jsonify(pkgs)
+
+
+@bp.route("/api/packages/<author>/<name>/")
+@is_package_page
+def package(package):
+ return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
+
+
+@bp.route("/api/packages/<author>/<name>/dependencies/")
+@is_package_page
+def package_dependencies(package):
+ ret = []
+
+ for dep in package.dependencies:
+ name = None
+ fulfilled_by = None
+
+ if dep.package:
+ name = dep.package.name
+ fulfilled_by = [ dep.package.getAsDictionaryKey() ]
+
+ elif dep.meta_package:
+ name = dep.meta_package.name
+ fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
+
+ else:
+ raise "Malformed dependency"
+
+ ret.append({
+ "name": name,
+ "is_optional": dep.optional,
+ "packages": fulfilled_by
+ })
+
+ return jsonify(ret)
+
+
+@bp.route("/api/topics/")
+def topics():
+ qb = QueryBuilder(request.args)
+ query = qb.buildTopicQuery(show_added=True)
+ return jsonify([t.getAsDictionary() for t in query.all()])
+
+
+@bp.route("/api/topic_discard/", methods=["POST"])
+@login_required
+def topic_set_discard():
+ tid = request.args.get("tid")
+ discard = request.args.get("discard")
+ if tid is None or discard is None:
+ abort(400)
+
+ topic = ForumTopic.query.get(tid)
+ if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
+ abort(403)
+
+ topic.discarded = discard == "true"
+ db.session.commit()
+
+ return jsonify(topic.getAsDictionary())
+
+
+@bp.route("/api/minetest_versions/")
+def versions():
+ return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
+ for rel in MinetestRelease.query.all() if rel.getActual() is not None])
+
+
+@bp.route("/api/whoami/")
+@is_api_authd
+def whoami(token):
+ if token is None:
+ return jsonify({ "is_authenticated": False, "username": None })
+ else:
+ return jsonify({ "is_authenticated": True, "username": token.owner.username })
diff --git a/app/blueprints/api/tokens.py b/app/blueprints/api/tokens.py
new file mode 100644
index 0000000..3f6b151
--- /dev/null
+++ b/app/blueprints/api/tokens.py
@@ -0,0 +1,141 @@
+# Content DB
+# Copyright (C) 2018 rubenwardy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+from flask import render_template, redirect, request, session, url_for
+from flask_user import login_required, current_user
+from . import bp
+from app.models import db, User, APIToken, Package, Permission
+from app.utils import randomString
+from app.querybuilder import QueryBuilder
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from wtforms.ext.sqlalchemy.fields import QuerySelectField
+
+class CreateAPIToken(FlaskForm):
+ name = StringField("Name", [InputRequired(), Length(1, 30)])
+ submit = SubmitField("Save")
+
+
+@bp.route("/users/<username>/tokens/")
+@login_required
+def list_tokens(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+
+ if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
+ abort(403)
+
+ return render_template("api/list_tokens.html", user=user)
+
+
+@bp.route("/users/<username>/tokens/new/", methods=["GET", "POST"])
+@bp.route("/users/<username>/tokens/<int:id>/edit/", methods=["GET", "POST"])
+@login_required
+def create_edit_token(username, id=None):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+
+ if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
+ abort(403)
+
+ is_new = id is None
+
+ token = None
+ access_token = None
+ if not is_new:
+ token = APIToken.query.get(id)
+ if token is None:
+ abort(404)
+ elif token.owner != user:
+ abort(403)
+
+ access_token = session.pop("token_" + str(id), None)
+
+ form = CreateAPIToken(formdata=request.form, obj=token)
+ if request.method == "POST" and form.validate():
+ if is_new:
+ token = APIToken()
+ token.owner = user
+ token.access_token = randomString(32)
+
+ form.populate_obj(token)
+ db.session.add(token)
+
+ db.session.commit() # save
+
+ # Store token so it can be shown in the edit page
+ session["token_" + str(token.id)] = token.access_token
+
+ return redirect(url_for("api.create_edit_token", username=username, id=token.id))
+
+ return render_template("api/create_edit_token.html", user=user, form=form, token=token, access_token=access_token)
+
+
+@bp.route("/users/<username>/tokens/<int:id>/reset/", methods=["POST"])
+@login_required
+def reset_token(username, id):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+
+ if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
+ abort(403)
+
+ is_new = id is None
+
+ token = APIToken.query.get(id)
+ if token is None:
+ abort(404)
+ elif token.owner != user:
+ abort(403)
+
+ token.access_token = randomString(32)
+
+ db.session.commit() # save
+
+ # Store token so it can be shown in the edit page
+ session["token_" + str(token.id)] = token.access_token
+
+ return redirect(url_for("api.create_edit_token", username=username, id=token.id))
+
+
+@bp.route("/users/<username>/tokens/<int:id>/delete/", methods=["POST"])
+@login_required
+def delete_token(username, id):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+
+ if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
+ abort(403)
+
+ is_new = id is None
+
+ token = APIToken.query.get(id)
+ if token is None:
+ abort(404)
+ elif token.owner != user:
+ abort(403)
+
+ db.session.delete(token)
+ db.session.commit()
+
+ return redirect(url_for("api.list_tokens", username=username))