diff --git a/app/__init__.py b/app/__init__.py index c0cbdad5..a913f57e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -117,6 +117,7 @@ def create_app(config_class=DevConfig): from app.views import notes_bp from app.views import users_bp from app.views import absences_bp + from app.api import bp as api_bp # https://scodoc.fr/ScoDoc app.register_blueprint(scodoc_bp) @@ -130,6 +131,7 @@ def create_app(config_class=DevConfig): app.register_blueprint( absences_bp, url_prefix="/ScoDoc//Scolarite/Absences" ) + app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") scodoc_exc_formatter = RequestFormatter( "[%(asctime)s] %(remote_addr)s requested %(url)s\n" "%(levelname)s in %(module)s: %(message)s" diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 00000000..34ebbc77 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,8 @@ +"""api.__init__ +""" + +from flask import Blueprint + +bp = Blueprint("api", __name__) + +from app.api import sco_api diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 00000000..0226976c --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,53 @@ +# -*- coding: UTF-8 -* +# Authentication code borrowed from Miguel Grinberg's Mega Tutorial +# (see https://github.com/miguelgrinberg/microblog) + +# Under The MIT License (MIT) + +# Copyright (c) 2017 Miguel Grinberg + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth +from app.auth.models import User +from app.api.errors import error_response + +basic_auth = HTTPBasicAuth() +token_auth = HTTPTokenAuth() + + +@basic_auth.verify_password +def verify_password(username, password): + user = User.query.filter_by(username=username).first() + if user and user.check_password(password): + return user + + +@basic_auth.error_handler +def basic_auth_error(status): + return error_response(status) + + +@token_auth.verify_token +def verify_token(token): + return User.check_token(token) if token else None + + +@token_auth.error_handler +def token_auth_error(status): + return error_response(status) diff --git a/app/api/errors.py b/app/api/errors.py new file mode 100644 index 00000000..ed8d0f3f --- /dev/null +++ b/app/api/errors.py @@ -0,0 +1,37 @@ +# Authentication code borrowed from Miguel Grinberg's Mega Tutorial +# (see https://github.com/miguelgrinberg/microblog) + +# Under The MIT License (MIT) + +# Copyright (c) 2017 Miguel Grinberg + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify +from werkzeug.http import HTTP_STATUS_CODES + + +def error_response(status_code, message=None): + payload = {"error": HTTP_STATUS_CODES.get(status_code, "Unknown error")} + if message: + payload["message"] = message + response = jsonify(payload) + response.status_code = status_code + return response + + +def bad_request(message): + return error_response(400, message) diff --git a/app/api/sco_api.py b/app/api/sco_api.py new file mode 100644 index 00000000..02b35090 --- /dev/null +++ b/app/api/sco_api.py @@ -0,0 +1,46 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""API ScoDoc 9 +""" +# PAS ENCORE IMPLEMENTEE, juste un essai + +from flask import jsonify, request, url_for, abort +from app import db +from app.api import bp +from app.api.auth import token_auth +from app.api.errors import bad_request + +from app import models + + +@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) +@token_auth.login_required +def list_depts(): + depts = models.Departement.query.filter_by(visible=True).all() + data = {"items": [d.to_dict() for d in depts]} + return jsonify(data) diff --git a/app/api/tokens.py b/app/api/tokens.py new file mode 100644 index 00000000..f36ec7b0 --- /dev/null +++ b/app/api/tokens.py @@ -0,0 +1,20 @@ +from flask import jsonify +from app import db +from app.api import bp +from app.api.auth import basic_auth, token_auth + + +@bp.route("/tokens", methods=["POST"]) +@basic_auth.login_required +def get_token(): + token = basic_auth.current_user().get_token() + db.session.commit() + return jsonify({"token": token}) + + +@bp.route("/tokens", methods=["DELETE"]) +@token_auth.login_required +def revoke_token(): + token_auth.current_user().revoke_token() + db.session.commit() + return "", 204 diff --git a/app/auth/routes.py b/app/auth/routes.py index 42cdc8e6..7b1712f0 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -37,9 +37,11 @@ def login(): if form.validate_on_submit(): user = User.query.filter_by(user_name=form.user_name.data).first() if user is None or not user.check_password(form.password.data): + current_app.logger.info("login: invalid (%s)", form.user_name.data) flash(_("Invalid user name or password")) return redirect(url_for("auth.login")) login_user(user, remember=form.remember_me.data) + current_app.logger.info("login: success (%s)", form.user_name.data) next_page = request.args.get("next") if not next_page or url_parse(next_page).netloc != "": next_page = url_for("scodoc.index") diff --git a/app/models/departements.py b/app/models/departements.py index 1dee2ca0..c0a928e3 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -34,3 +34,13 @@ class Departement(db.Model): def __repr__(self): return f"" + + def to_dict(self): + data = { + "id": self.id, + "acronym": self.acronym, + "description": self.description, + "visible": self.visible, + "date_creation": self.date_creation, + } + return data diff --git a/misc/example-api-1.py b/misc/example-api-1.py index d823aac7..171365e5 100644 --- a/misc/example-api-1.py +++ b/misc/example-api-1.py @@ -52,6 +52,10 @@ def POST(s, path, data, errmsg=None): # --- Ouverture session (login) s = requests.Session() +s.post( + "https://deb11.viennet.net/api/auth/login", + data={"user_name": USER, "password": PASSWORD}, +) r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE) if r.status_code != 200: raise ScoError("erreur de connection: vérifier adresse et identifiants") diff --git a/requirements-3.9.txt b/requirements-3.9.txt index 0ba9dcbf..682b0505 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -18,6 +18,7 @@ Flask==2.0.1 Flask-Babel==2.0.0 Flask-Bootstrap==3.3.7.1 Flask-Caching==1.10.1 +Flask-HTTPAuth==4.4.0 Flask-Login==0.5.0 Flask-Mail==0.9.1 Flask-Migrate==3.1.0 diff --git a/sco_version.py b/sco_version.py index 2e77dbe6..fda1b3c7 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.0.11" +SCOVERSION = "9.0.12" SCONAME = "ScoDoc"