diff options
| author | rubenwardy <rw@rubenwardy.com> | 2019-11-22 14:33:22 +0000 |
|---|---|---|
| committer | rubenwardy <rw@rubenwardy.com> | 2019-11-27 01:06:58 +0000 |
| commit | 4ce388c8aa5d5502408609983535a9812d41d6d1 (patch) | |
| tree | 5ad9123949ca2068dfe975284d0f1b3acdf5b437 /app/blueprints/api | |
| parent | cb5451fe5d49e0eda379e3cd636c54e8ea1a3f8e (diff) | |
| download | cheatdb-4ce388c8aa5d5502408609983535a9812d41d6d1.tar.xz | |
Add API Token creation
Diffstat (limited to 'app/blueprints/api')
| -rw-r--r-- | app/blueprints/api/__init__.py | 83 | ||||
| -rw-r--r-- | app/blueprints/api/auth.py | 42 | ||||
| -rw-r--r-- | app/blueprints/api/endpoints.py | 109 | ||||
| -rw-r--r-- | app/blueprints/api/tokens.py | 141 |
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)) |
